Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 14c76abe

History | View | Annotate | Download (33.8 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.decorators import login_required
44
from django.contrib.auth.models import User
45
from django.core.urlresolvers import reverse
46
from django.db import transaction
47
from django.http import HttpResponse, HttpResponseRedirect, Http404
48
from django.shortcuts import redirect
49
from django.utils.translation import ugettext as _
50
from django.core.exceptions import PermissionDenied
51
from django.views.decorators.http import require_http_methods
52
from django.utils import simplejson as json
53

    
54
from synnefo.lib import join_urls
55

    
56
import astakos.im.messages as astakos_messages
57

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

    
76
logger = logging.getLogger(__name__)
77

    
78

    
79
@require_http_methods(["GET", "POST"])
80
@cookie_fix
81
@signed_terms_required
82
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
83
    """
84
    If there is logged on user renders the profile page otherwise renders login page.
85

86
    **Arguments**
87

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

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

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

99
    **Template:**
100

101
    im/profile.html or im/login.html or ``template_name`` keyword argument.
102

103
    """
104
    extra_context = extra_context or {}
105
    template_name = login_template_name
106
    if request.user.is_authenticated():
107
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
108

    
109
    third_party_token = request.GET.get('key', False)
110
    if third_party_token:
111
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
112

    
113
    return render_response(
114
        template_name,
115
        login_form = LoginForm(request=request),
116
        context_instance = get_context(request, extra_context)
117
    )
118

    
119

    
120
@require_http_methods(["POST"])
121
@cookie_fix
122
@valid_astakos_user_required
123
def update_token(request):
124
    """
125
    Update api token view.
126
    """
127
    user = request.user
128
    user.renew_token()
129
    user.save()
130
    messages.success(request, astakos_messages.TOKEN_UPDATED)
131
    return HttpResponseRedirect(reverse('api_access'))
132

    
133

    
134
@require_http_methods(["GET", "POST"])
135
@cookie_fix
136
@valid_astakos_user_required
137
@transaction.commit_manually
138
def invite(request, template_name='im/invitations.html', extra_context=None):
139
    """
140
    Allows a user to invite somebody else.
141

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

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

149
    If the user isn't logged in, redirects to settings.LOGIN_URL.
150

151
    **Arguments**
152

153
    ``template_name``
154
        A custom template to use. This is optional; if not specified,
155
        this will default to ``im/invitations.html``.
156

157
    ``extra_context``
158
        An dictionary of variables to add to the template context.
159

160
    **Template:**
161

162
    im/invitations.html or ``template_name`` keyword argument.
163

164
    **Settings:**
165

166
    The view expectes the following settings are defined:
167

168
    * LOGIN_URL: login uri
169
    """
170
    extra_context = extra_context or {}
171
    status = None
172
    message = None
173
    form = InvitationForm()
174

    
175
    inviter = request.user
176
    if request.method == 'POST':
177
        form = InvitationForm(request.POST)
178
        if inviter.invitations > 0:
179
            if form.is_valid():
180
                try:
181
                    email = form.cleaned_data.get('username')
182
                    realname = form.cleaned_data.get('realname')
183
                    invite_func(inviter, email, realname)
184
                    message = _(astakos_messages.INVITATION_SENT) % locals()
185
                    messages.success(request, message)
186
                except Exception, e:
187
                    transaction.rollback()
188
                    raise
189
                else:
190
                    transaction.commit()
191
        else:
192
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
193
            messages.error(request, message)
194

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

    
206

    
207
@require_http_methods(["GET", "POST"])
208
@required_auth_methods_assigned()
209
@login_required
210
@cookie_fix
211
@signed_terms_required
212
def api_access(request, template_name='im/api_access.html',
213
               extra_context=None):
214
    """
215
    API access view.
216
    """
217
    context = {}
218

    
219
    token_url = join_urls(settings.BASE_URL, settings.BASE_PATH,
220
                           reverse('tokens_authenticate'))
221
    context['services'] = Component.catalog()
222
    context['token_url'] = token_url
223
    context['client_url'] = settings.API_CLIENT_URL
224

    
225
    if extra_context:
226
        context.update(extra_context)
227
    context_instance = get_context(request, context)
228
    return render_response(template_name,
229
                           context_instance=context_instance)
230

    
231

    
232
@require_http_methods(["GET", "POST"])
233
@required_auth_methods_assigned(allow_access=True)
234
@login_required
235
@cookie_fix
236
@signed_terms_required
237
def edit_profile(request, template_name='im/profile.html', extra_context=None):
238
    """
239
    Allows a user to edit his/her profile.
240

241
    In case of GET request renders a form for displaying the user information.
242
    In case of POST updates the user informantion and redirects to ``next``
243
    url parameter if exists.
244

245
    If the user isn't logged in, redirects to settings.LOGIN_URL.
246

247
    **Arguments**
248

249
    ``template_name``
250
        A custom template to use. This is optional; if not specified,
251
        this will default to ``im/profile.html``.
252

253
    ``extra_context``
254
        An dictionary of variables to add to the template context.
255

256
    **Template:**
257

258
    im/profile.html or ``template_name`` keyword argument.
259

260
    **Settings:**
261

262
    The view expectes the following settings are defined:
263

264
    * LOGIN_URL: login uri
265
    """
266
    extra_context = extra_context or {}
267
    form = ProfileForm(
268
        instance=request.user,
269
        session_key=request.session.session_key
270
    )
271
    extra_context['next'] = request.GET.get('next')
272
    if request.method == 'POST':
273
        form = ProfileForm(
274
            request.POST,
275
            instance=request.user,
276
            session_key=request.session.session_key
277
        )
278
        if form.is_valid():
279
            try:
280
                prev_token = request.user.auth_token
281
                user = form.save(request=request)
282
                next = restrict_next(
283
                    request.POST.get('next'),
284
                    domain=settings.COOKIE_DOMAIN
285
                )
286
                msg = _(astakos_messages.PROFILE_UPDATED)
287
                messages.success(request, msg)
288

    
289
                if form.email_changed:
290
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
291
                    messages.success(request, msg)
292
                if form.password_changed:
293
                    msg = _(astakos_messages.PASSWORD_CHANGED)
294
                    messages.success(request, msg)
295

    
296
                if next:
297
                    return redirect(next)
298
                else:
299
                    return redirect(reverse('edit_profile'))
300
            except ValueError, ve:
301
                messages.success(request, ve)
302
    elif request.method == "GET":
303
        request.user.is_verified = True
304
        request.user.save()
305

    
306
    # existing providers
307
    user_providers = request.user.get_enabled_auth_providers()
308
    user_disabled_providers = request.user.get_disabled_auth_providers()
309

    
310
    # providers that user can add
311
    user_available_providers = request.user.get_available_auth_providers()
312

    
313
    extra_context['services'] = Component.catalog().values()
314
    return render_response(template_name,
315
                           profile_form=form,
316
                           user_providers=user_providers,
317
                           user_disabled_providers=user_disabled_providers,
318
                           user_available_providers=user_available_providers,
319
                           context_instance=get_context(request,
320
                                                          extra_context))
321

    
322

    
323
@transaction.commit_manually
324
@require_http_methods(["GET", "POST"])
325
@cookie_fix
326
def signup(request, template_name='im/signup.html', on_success='index',
327
           extra_context=None, activation_backend=None):
328
    """
329
    Allows a user to create a local account.
330

331
    In case of GET request renders a form for entering the user information.
332
    In case of POST handles the signup.
333

334
    The user activation will be delegated to the backend specified by the
335
    ``activation_backend`` keyword argument if present, otherwise to the
336
    ``astakos.im.activation_backends.InvitationBackend`` if
337
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
338
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
339
    activation_backends);
340

341
    Upon successful user creation, if ``next`` url parameter is present the
342
    user is redirected there otherwise renders the same page with a success
343
    message.
344

345
    On unsuccessful creation, renders ``template_name`` with an error message.
346

347
    **Arguments**
348

349
    ``template_name``
350
        A custom template to render. This is optional;
351
        if not specified, this will default to ``im/signup.html``.
352

353
    ``extra_context``
354
        An dictionary of variables to add to the template context.
355

356
    ``on_success``
357
        Resolvable view name to redirect on registration success.
358

359
    **Template:**
360

361
    im/signup.html or ``template_name`` keyword argument.
362
    """
363
    extra_context = extra_context or {}
364
    if request.user.is_authenticated():
365
        logger.info("%s already signed in, redirect to index",
366
                    request.user.log_display)
367
        return HttpResponseRedirect(reverse('index'))
368

    
369
    provider = get_query(request).get('provider', 'local')
370
    if not auth.get_provider(provider).get_create_policy:
371
        logger.error("%s provider not available for signup", provider)
372
        raise PermissionDenied
373

    
374
    instance = None
375

    
376
    # user registered using third party provider
377
    third_party_token = request.REQUEST.get('third_party_token', None)
378
    unverified = None
379
    if third_party_token:
380
        # retreive third party entry. This was created right after the initial
381
        # third party provider handshake.
382
        pending = get_object_or_404(PendingThirdPartyUser,
383
                                    token=third_party_token)
384

    
385
        provider = pending.provider
386

    
387
        # clone third party instance into the corresponding AstakosUser
388
        instance = pending.get_user_instance()
389
        get_unverified = AstakosUserAuthProvider.objects.unverified
390

    
391
        # check existing unverified entries
392
        unverified = get_unverified(pending.provider,
393
                                    identifier=pending.third_party_identifier)
394

    
395
        if unverified and request.method == 'GET':
396
            messages.warning(request, unverified.get_pending_registration_msg)
397
            if unverified.user.moderated:
398
                messages.warning(request,
399
                                 unverified.get_pending_resend_activation_msg)
400
            else:
401
                messages.warning(request,
402
                                 unverified.get_pending_moderation_msg)
403

    
404
    # prepare activation backend based on current request
405
    if not activation_backend:
406
        activation_backend = activation_backends.get_backend()
407

    
408
    form_kwargs = {'instance': instance}
409
    if third_party_token:
410
        form_kwargs['third_party_token'] = third_party_token
411

    
412
    form = activation_backend.get_signup_form(
413
        provider, None, **form_kwargs)
414

    
415
    if request.method == 'POST':
416
        form = activation_backend.get_signup_form(
417
            provider,
418
            request.POST,
419
            **form_kwargs)
420

    
421
        if form.is_valid():
422
            commited = False
423
            try:
424
                user = form.save(commit=False)
425

    
426
                # delete previously unverified accounts
427
                if AstakosUser.objects.user_exists(user.email):
428
                    AstakosUser.objects.get_by_identifier(user.email).delete()
429

    
430
                # store_user so that user auth providers get initialized
431
                form.store_user(user, request)
432
                result = activation_backend.handle_registration(user)
433
                if result.status == \
434
                        activation_backend.Result.PENDING_MODERATION:
435
                    # user should be warned that his account is not active yet
436
                    status = messages.WARNING
437
                else:
438
                    status = messages.SUCCESS
439
                message = result.message
440
                activation_backend.send_result_notifications(result, user)
441

    
442
                # commit user entry
443
                transaction.commit()
444
                # commited flag
445
                # in case an exception get raised from this point
446
                commited = True
447

    
448
                if user and user.is_active:
449
                    # activation backend directly activated the user
450
                    # log him in
451
                    next = request.POST.get('next', '')
452
                    response = prepare_response(request, user, next=next)
453
                    return response
454

    
455
                messages.add_message(request, status, message)
456
                return HttpResponseRedirect(reverse(on_success))
457
            except Exception, e:
458
                if not commited:
459
                    transaction.rollback()
460
                raise
461

    
462
    return render_response(template_name,
463
                           signup_form=form,
464
                           third_party_token=third_party_token,
465
                           provider=provider,
466
                           context_instance=get_context(request, extra_context))
467

    
468

    
469
@require_http_methods(["GET", "POST"])
470
@required_auth_methods_assigned(allow_access=True)
471
@login_required
472
@cookie_fix
473
@signed_terms_required
474
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
475
    """
476
    Allows a user to send feedback.
477

478
    In case of GET request renders a form for providing the feedback information.
479
    In case of POST sends an email to support team.
480

481
    If the user isn't logged in, redirects to settings.LOGIN_URL.
482

483
    **Arguments**
484

485
    ``template_name``
486
        A custom template to use. This is optional; if not specified,
487
        this will default to ``im/feedback.html``.
488

489
    ``extra_context``
490
        An dictionary of variables to add to the template context.
491

492
    **Template:**
493

494
    im/signup.html or ``template_name`` keyword argument.
495

496
    **Settings:**
497

498
    * LOGIN_URL: login uri
499
    """
500
    extra_context = extra_context or {}
501
    if request.method == 'GET':
502
        form = FeedbackForm()
503
    if request.method == 'POST':
504
        if not request.user:
505
            return HttpResponse('Unauthorized', status=401)
506

    
507
        form = FeedbackForm(request.POST)
508
        if form.is_valid():
509
            msg = form.cleaned_data['feedback_msg']
510
            data = form.cleaned_data['feedback_data']
511
            send_feedback(msg, data, request.user, email_template_name)
512
            message = _(astakos_messages.FEEDBACK_SENT)
513
            messages.success(request, message)
514
            return HttpResponseRedirect(reverse('feedback'))
515

    
516
    return render_response(template_name,
517
                           feedback_form=form,
518
                           context_instance=get_context(request,
519
                                                        extra_context))
520

    
521

    
522
@require_http_methods(["GET"])
523
@cookie_fix
524
def logout(request, template='registration/logged_out.html',
525
           extra_context=None):
526
    """
527
    Wraps `django.contrib.auth.logout`.
528
    """
529
    extra_context = extra_context or {}
530
    response = HttpResponse()
531
    if request.user.is_authenticated():
532
        email = request.user.email
533
        auth_logout(request)
534
    else:
535
        response['Location'] = reverse('index')
536
        response.status_code = 301
537
        return response
538

    
539
    next = restrict_next(
540
        request.GET.get('next'),
541
        domain=settings.COOKIE_DOMAIN
542
    )
543

    
544
    if next:
545
        response['Location'] = next
546
        response.status_code = 302
547
    elif settings.LOGOUT_NEXT:
548
        response['Location'] = settings.LOGOUT_NEXT
549
        response.status_code = 301
550
    else:
551
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
552
        provider = auth.get_provider(last_provider)
553
        message = provider.get_logout_success_msg
554
        extra = provider.get_logout_success_extra_msg
555
        if extra:
556
            message += "<br />"  + extra
557
        messages.success(request, message)
558
        response['Location'] = reverse('index')
559
        response.status_code = 301
560
    return response
561

    
562

    
563
@require_http_methods(["GET", "POST"])
564
@cookie_fix
565
@transaction.commit_manually
566
def activate(request, greeting_email_template_name='im/welcome_email.txt',
567
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
568
    """
569
    Activates the user identified by the ``auth`` request parameter, sends a
570
    welcome email and renews the user token.
571

572
    The view uses commit_manually decorator in order to ensure the user state
573
    will be updated only if the email will be send successfully.
574
    """
575
    token = request.GET.get('auth')
576
    next = request.GET.get('next')
577

    
578
    if request.user.is_authenticated():
579
        message = _(astakos_messages.LOGGED_IN_WARNING)
580
        messages.error(request, message)
581
        return HttpResponseRedirect(reverse('index'))
582

    
583
    try:
584
        user = AstakosUser.objects.get(verification_code=token)
585
    except AstakosUser.DoesNotExist:
586
        raise Http404
587

    
588
    if user.email_verified:
589
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
590
        messages.error(request, message)
591
        return HttpResponseRedirect(reverse('index'))
592

    
593
    try:
594
        backend = activation_backends.get_backend()
595
        result = backend.handle_verification(user, token)
596
        backend.send_result_notifications(result, user)
597
        next = settings.ACTIVATION_REDIRECT_URL or next
598
        response = HttpResponseRedirect(reverse('index'))
599
        if user.is_active:
600
            response = prepare_response(request, user, next, renew=True)
601
            messages.success(request, _(result.message))
602
        else:
603
            messages.warning(request, _(result.message))
604
    except Exception:
605
        transaction.rollback()
606
        raise
607
    else:
608
        transaction.commit()
609
        return response
610

    
611

    
612
@require_http_methods(["GET", "POST"])
613
@cookie_fix
614
def approval_terms(request, term_id=None,
615
                   template_name='im/approval_terms.html', extra_context=None):
616
    extra_context = extra_context or {}
617
    term = None
618
    terms = None
619
    if not term_id:
620
        try:
621
            term = ApprovalTerms.objects.order_by('-id')[0]
622
        except IndexError:
623
            pass
624
    else:
625
        try:
626
            term = ApprovalTerms.objects.get(id=term_id)
627
        except ApprovalTerms.DoesNotExist, e:
628
            pass
629

    
630
    if not term:
631
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
632
        return HttpResponseRedirect(reverse('index'))
633
    try:
634
        f = open(term.location, 'r')
635
    except IOError:
636
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
637
        return render_response(
638
            template_name, context_instance=get_context(request,
639
                                                        extra_context))
640

    
641
    terms = f.read()
642

    
643
    if request.method == 'POST':
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
    else:
660
        form = None
661
        if request.user.is_authenticated() and not request.user.signed_terms:
662
            form = SignApprovalTermsForm(instance=request.user)
663
        return render_response(template_name,
664
                               terms=terms,
665
                               approval_terms_form=form,
666
                               context_instance=get_context(request,
667
                                                            extra_context))
668

    
669

    
670
@require_http_methods(["GET", "POST"])
671
@cookie_fix
672
@transaction.commit_manually
673
def change_email(request, activation_key=None,
674
                 email_template_name='registration/email_change_email.txt',
675
                 form_template_name='registration/email_change_form.html',
676
                 confirm_template_name='registration/email_change_done.html',
677
                 extra_context=None):
678
    extra_context = extra_context or {}
679

    
680
    if not settings.EMAILCHANGE_ENABLED:
681
        raise PermissionDenied
682

    
683
    if activation_key:
684
        try:
685
            try:
686
                email_change = EmailChange.objects.get(
687
                    activation_key=activation_key)
688
            except EmailChange.DoesNotExist:
689
                transaction.rollback()
690
                logger.error("[change-email] Invalid or used activation "
691
                             "code, %s", activation_key)
692
                raise Http404
693

    
694
            if (request.user.is_authenticated() and \
695
                request.user == email_change.user) or not \
696
                    request.user.is_authenticated():
697
                user = EmailChange.objects.change_email(activation_key)
698
                msg = _(astakos_messages.EMAIL_CHANGED)
699
                messages.success(request, msg)
700
                transaction.commit()
701
                return HttpResponseRedirect(reverse('edit_profile'))
702
            else:
703
                logger.error("[change-email] Access from invalid user, %s %s",
704
                             email_change.user, request.user.log_display)
705
                transaction.rollback()
706
                raise PermissionDenied
707
        except ValueError, e:
708
            messages.error(request, e)
709
            transaction.rollback()
710
            return HttpResponseRedirect(reverse('index'))
711

    
712
        return render_response(confirm_template_name,
713
                               modified_user=user if 'user' in locals()
714
                               else None, context_instance=get_context(request,
715
                               extra_context))
716

    
717
    if not request.user.is_authenticated():
718
        path = quote(request.get_full_path())
719
        url = request.build_absolute_uri(reverse('index'))
720
        return HttpResponseRedirect(url + '?next=' + path)
721

    
722
    # clean up expired email changes
723
    if request.user.email_change_is_pending():
724
        change = request.user.emailchanges.get()
725
        if change.activation_key_expired():
726
            change.delete()
727
            transaction.commit()
728
            return HttpResponseRedirect(reverse('email_change'))
729

    
730
    form = EmailChangeForm(request.POST or None)
731
    if request.method == 'POST' and form.is_valid():
732
        try:
733
            ec = form.save(request, email_template_name, request)
734
        except Exception, e:
735
            transaction.rollback()
736
            raise
737
        else:
738
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
739
            messages.success(request, msg)
740
            transaction.commit()
741
            return HttpResponseRedirect(reverse('edit_profile'))
742

    
743
    if request.user.email_change_is_pending():
744
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
745

    
746
    return render_response(
747
        form_template_name,
748
        form=form,
749
        context_instance=get_context(request, extra_context)
750
    )
751

    
752

    
753
@cookie_fix
754
def send_activation(request, user_id, template_name='im/login.html',
755
                    extra_context=None):
756

    
757
    if request.user.is_authenticated():
758
        return HttpResponseRedirect(reverse('index'))
759

    
760
    extra_context = extra_context or {}
761
    try:
762
        u = AstakosUser.objects.get(id=user_id)
763
    except AstakosUser.DoesNotExist:
764
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
765
    else:
766
        if u.email_verified:
767
            logger.warning("[resend activation] Account already verified: %s",
768
                           u.log_display)
769

    
770
            messages.error(request,
771
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
772
        else:
773
            activation_backend = activation_backends.get_backend()
774
            activation_backend.send_user_verification_email(u)
775
            messages.success(request, astakos_messages.ACTIVATION_SENT)
776

    
777
    return HttpResponseRedirect(reverse('index'))
778

    
779

    
780
@require_http_methods(["GET"])
781
@cookie_fix
782
@valid_astakos_user_required
783
def resource_usage(request):
784

    
785
    resources_meta = presentation.RESOURCES
786

    
787
    current_usage = quotas.get_user_quotas(request.user)
788
    current_usage = json.dumps(current_usage['system'])
789
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
790
    if resource_catalog is False:
791
        # on fail resource_groups contains the result object
792
        result = resource_groups
793
        messages.error(request, 'Unable to retrieve system resources: %s' %
794
                       result.reason)
795

    
796
    resource_catalog = json.dumps(resource_catalog)
797
    resource_groups = json.dumps(resource_groups)
798
    resources_order = json.dumps(resources_meta.get('resources_order'))
799

    
800
    return render_response('im/resource_usage.html',
801
                           context_instance=get_context(request),
802
                           resource_catalog=resource_catalog,
803
                           resource_groups=resource_groups,
804
                           resources_order=resources_order,
805
                           current_usage=current_usage,
806
                           token_cookie_name=settings.COOKIE_NAME,
807
                           usage_update_interval=
808
                           settings.USAGE_UPDATE_INTERVAL)
809

    
810

    
811
# TODO: action only on POST and user should confirm the removal
812
@require_http_methods(["POST"])
813
@cookie_fix
814
@valid_astakos_user_required
815
def remove_auth_provider(request, pk):
816
    try:
817
        provider = request.user.auth_providers.get(pk=int(pk)).settings
818
    except AstakosUserAuthProvider.DoesNotExist:
819
        raise Http404
820

    
821
    if provider.get_remove_policy:
822
        messages.success(request, provider.get_removed_msg)
823
        provider.remove_from_user()
824
        return HttpResponseRedirect(reverse('edit_profile'))
825
    else:
826
        raise PermissionDenied
827

    
828

    
829
@require_http_methods(["GET"])
830
@required_auth_methods_assigned(allow_access=True)
831
@login_required
832
@cookie_fix
833
@signed_terms_required
834
def landing(request):
835
    context = {'services': Component.catalog(orderfor='dashboard')}
836
    return render_response(
837
        'im/landing.html',
838
        context_instance=get_context(request), **context)
839

    
840

    
841
@cookie_fix
842
def get_menu(request, with_extra_links=False, with_signout=True):
843
    user = request.user
844
    index_url = reverse('index')
845

    
846
    if isinstance(user, User) and user.is_authenticated():
847
        l = []
848
        append = l.append
849
        item = MenuItem
850
        item.current_path = request.build_absolute_uri(request.path)
851
        append(item(url=request.build_absolute_uri(reverse('index')),
852
                    name=user.email))
853
        if with_extra_links:
854
            append(item(url=request.build_absolute_uri(reverse('landing')),
855
                        name="Overview"))
856
        if with_signout:
857
            append(item(url=request.build_absolute_uri(reverse('landing')),
858
                        name="Dashboard"))
859
        if with_extra_links:
860
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
861
                        name="Profile"))
862

    
863
        if with_extra_links:
864
            if settings.INVITATIONS_ENABLED:
865
                append(item(url=request.build_absolute_uri(reverse('invite')),
866
                            name="Invitations"))
867

    
868
            append(item(url=request.build_absolute_uri(reverse('api_access')),
869
                        name="API access"))
870

    
871
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
872
                        name="Usage"))
873

    
874
            if settings.PROJECTS_VISIBLE:
875
                append(item(url=request.build_absolute_uri(reverse('project_list')),
876
                            name="Projects"))
877

    
878
            append(item(url=request.build_absolute_uri(reverse('feedback')),
879
                        name="Contact"))
880
        if with_signout:
881
            append(item(url=request.build_absolute_uri(reverse('logout')),
882
                        name="Sign out"))
883
    else:
884
        l = [{'url': request.build_absolute_uri(index_url),
885
              'name': _("Sign in")}]
886

    
887
    callback = request.GET.get('callback', None)
888
    data = json.dumps(tuple(l))
889
    mimetype = 'application/json'
890

    
891
    if callback:
892
        mimetype = 'application/javascript'
893
        data = '%s(%s)' % (callback, data)
894

    
895
    return HttpResponse(content=data, mimetype=mimetype)
896

    
897

    
898
class MenuItem(dict):
899
    current_path = ''
900

    
901
    def __init__(self, *args, **kwargs):
902
        super(MenuItem, self).__init__(*args, **kwargs)
903
        if kwargs.get('url') or kwargs.get('submenu'):
904
            self.__set_is_active__()
905

    
906
    def __setitem__(self, key, value):
907
        super(MenuItem, self).__setitem__(key, value)
908
        if key in ('url', 'submenu'):
909
            self.__set_is_active__()
910

    
911
    def __set_is_active__(self):
912
        if self.get('is_active'):
913
            return
914
        if self.current_path.startswith(self.get('url')):
915
            self.__setitem__('is_active', True)
916
        else:
917
            submenu = self.get('submenu', ())
918
            current = (i for i in submenu if i.get('url') == self.current_path)
919
            try:
920
                current_node = current.next()
921
                if not current_node.get('is_active'):
922
                    current_node.__setitem__('is_active', True)
923
                self.__setitem__('is_active', True)
924
            except StopIteration:
925
                return
926

    
927
    def __setattribute__(self, name, value):
928
        super(MenuItem, self).__setattribute__(name, value)
929
        if name == 'current_path':
930
            self.__set_is_active__()
931

    
932

    
933
def get_services(request):
934
    callback = request.GET.get('callback', None)
935
    mimetype = 'application/json'
936
    data = json.dumps(Component.catalog().values())
937

    
938
    if callback:
939
        # Consume session messages. When get_services is loaded from an astakos
940
        # page, messages should have already been consumed in the html
941
        # response. When get_services is loaded from another domain/service we
942
        # consume them here so that no stale messages to appear if user visits
943
        # an astakos view later on.
944
        # TODO: messages could be served to other services/sites in the dict
945
        # response of get_services and/or get_menu. Services could handle those
946
        # messages respectively.
947
        messages_list = list(messages.get_messages(request))
948
        mimetype = 'application/javascript'
949
        data = '%s(%s)' % (callback, data)
950

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