Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 58350a50

History | View | Annotate | Download (33.1 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.contrib.auth.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
import astakos.im.messages as astakos_messages
55

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

    
74
logger = logging.getLogger(__name__)
75

    
76

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

84
    **Arguments**
85

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

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

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

97
    **Template:**
98

99
    im/profile.html or im/login.html or ``template_name`` keyword argument.
100

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

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

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

    
117

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

    
131

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

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

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

147
    If the user isn't logged in, redirects to settings.LOGIN_URL.
148

149
    **Arguments**
150

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

155
    ``extra_context``
156
        An dictionary of variables to add to the template context.
157

158
    **Template:**
159

160
    im/invitations.html or ``template_name`` keyword argument.
161

162
    **Settings:**
163

164
    The view expectes the following settings are defined:
165

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

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

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

    
204

    
205
@require_http_methods(["GET", "POST"])
206
@required_auth_methods_assigned(allow_access=True)
207
@login_required
208
@cookie_fix
209
@signed_terms_required
210
def edit_profile(request, template_name='im/profile.html', extra_context=None):
211
    """
212
    Allows a user to edit his/her profile.
213

214
    In case of GET request renders a form for displaying the user information.
215
    In case of POST updates the user informantion and redirects to ``next``
216
    url parameter if exists.
217

218
    If the user isn't logged in, redirects to settings.LOGIN_URL.
219

220
    **Arguments**
221

222
    ``template_name``
223
        A custom template to use. This is optional; if not specified,
224
        this will default to ``im/profile.html``.
225

226
    ``extra_context``
227
        An dictionary of variables to add to the template context.
228

229
    **Template:**
230

231
    im/profile.html or ``template_name`` keyword argument.
232

233
    **Settings:**
234

235
    The view expectes the following settings are defined:
236

237
    * LOGIN_URL: login uri
238
    """
239
    extra_context = extra_context or {}
240
    form = ProfileForm(
241
        instance=request.user,
242
        session_key=request.session.session_key
243
    )
244
    extra_context['next'] = request.GET.get('next')
245
    if request.method == 'POST':
246
        form = ProfileForm(
247
            request.POST,
248
            instance=request.user,
249
            session_key=request.session.session_key
250
        )
251
        if form.is_valid():
252
            try:
253
                prev_token = request.user.auth_token
254
                user = form.save(request=request)
255
                next = restrict_next(
256
                    request.POST.get('next'),
257
                    domain=settings.COOKIE_DOMAIN
258
                )
259
                msg = _(astakos_messages.PROFILE_UPDATED)
260
                messages.success(request, msg)
261

    
262
                if form.email_changed:
263
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
264
                    messages.success(request, msg)
265
                if form.password_changed:
266
                    msg = _(astakos_messages.PASSWORD_CHANGED)
267
                    messages.success(request, msg)
268

    
269
                if next:
270
                    return redirect(next)
271
                else:
272
                    return redirect(reverse('edit_profile'))
273
            except ValueError, ve:
274
                messages.success(request, ve)
275
    elif request.method == "GET":
276
        request.user.is_verified = True
277
        request.user.save()
278

    
279
    # existing providers
280
    user_providers = request.user.get_enabled_auth_providers()
281
    user_disabled_providers = request.user.get_disabled_auth_providers()
282

    
283
    # providers that user can add
284
    user_available_providers = request.user.get_available_auth_providers()
285

    
286
    extra_context['services'] = Service.catalog().values()
287
    return render_response(template_name,
288
                           profile_form=form,
289
                           user_providers=user_providers,
290
                           user_disabled_providers=user_disabled_providers,
291
                           user_available_providers=user_available_providers,
292
                           context_instance=get_context(request,
293
                                                          extra_context))
294

    
295

    
296
@transaction.commit_manually
297
@require_http_methods(["GET", "POST"])
298
@cookie_fix
299
def signup(request, template_name='im/signup.html', on_success='index',
300
           extra_context=None, activation_backend=None):
301
    """
302
    Allows a user to create a local account.
303

304
    In case of GET request renders a form for entering the user information.
305
    In case of POST handles the signup.
306

307
    The user activation will be delegated to the backend specified by the
308
    ``activation_backend`` keyword argument if present, otherwise to the
309
    ``astakos.im.activation_backends.InvitationBackend`` if
310
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
311
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
312
    activation_backends);
313

314
    Upon successful user creation, if ``next`` url parameter is present the
315
    user is redirected there otherwise renders the same page with a success
316
    message.
317

318
    On unsuccessful creation, renders ``template_name`` with an error message.
319

320
    **Arguments**
321

322
    ``template_name``
323
        A custom template to render. This is optional;
324
        if not specified, this will default to ``im/signup.html``.
325

326
    ``extra_context``
327
        An dictionary of variables to add to the template context.
328

329
    ``on_success``
330
        Resolvable view name to redirect on registration success.
331

332
    **Template:**
333

334
    im/signup.html or ``template_name`` keyword argument.
335
    """
336
    extra_context = extra_context or {}
337
    if request.user.is_authenticated():
338
        logger.info("%s already signed in, redirect to index",
339
                    request.user.log_display)
340
        return HttpResponseRedirect(reverse('index'))
341

    
342
    provider = get_query(request).get('provider', 'local')
343
    if not auth.get_provider(provider).get_create_policy:
344
        logger.error("%s provider not available for signup", provider)
345
        raise PermissionDenied
346

    
347
    instance = None
348

    
349
    # user registered using third party provider
350
    third_party_token = request.REQUEST.get('third_party_token', None)
351
    unverified = None
352
    if third_party_token:
353
        # retreive third party entry. This was created right after the initial
354
        # third party provider handshake.
355
        pending = get_object_or_404(PendingThirdPartyUser,
356
                                    token=third_party_token)
357

    
358
        provider = pending.provider
359

    
360
        # clone third party instance into the corresponding AstakosUser
361
        instance = pending.get_user_instance()
362
        get_unverified = AstakosUserAuthProvider.objects.unverified
363

    
364
        # check existing unverified entries
365
        unverified = get_unverified(pending.provider,
366
                                    identifier=pending.third_party_identifier)
367

    
368
        if unverified and request.method == 'GET':
369
            messages.warning(request, unverified.get_pending_registration_msg)
370
            if unverified.user.moderated:
371
                messages.warning(request,
372
                                 unverified.get_pending_resend_activation_msg)
373
            else:
374
                messages.warning(request,
375
                                 unverified.get_pending_moderation_msg)
376

    
377
    # prepare activation backend based on current request
378
    if not activation_backend:
379
        activation_backend = activation_backends.get_backend()
380

    
381
    form_kwargs = {'instance': instance}
382
    if third_party_token:
383
        form_kwargs['third_party_token'] = third_party_token
384

    
385
    form = activation_backend.get_signup_form(
386
        provider, None, **form_kwargs)
387

    
388
    if request.method == 'POST':
389
        form = activation_backend.get_signup_form(
390
            provider,
391
            request.POST,
392
            **form_kwargs)
393

    
394
        if form.is_valid():
395
            commited = False
396
            try:
397
                user = form.save(commit=False)
398

    
399
                # delete previously unverified accounts
400
                if AstakosUser.objects.user_exists(user.email):
401
                    AstakosUser.objects.get_by_identifier(user.email).delete()
402

    
403
                # store_user so that user auth providers get initialized
404
                form.store_user(user, request)
405
                result = activation_backend.handle_registration(user)
406
                if result.status == \
407
                        activation_backend.Result.PENDING_MODERATION:
408
                    # user should be warned that his account is not active yet
409
                    status = messages.WARNING
410
                else:
411
                    status = messages.SUCCESS
412
                message = result.message
413
                activation_backend.send_result_notifications(result, user)
414

    
415
                # commit user entry
416
                transaction.commit()
417
                # commited flag
418
                # in case an exception get raised from this point
419
                commited = True
420

    
421
                if user and user.is_active:
422
                    # activation backend directly activated the user
423
                    # log him in
424
                    next = request.POST.get('next', '')
425
                    response = prepare_response(request, user, next=next)
426
                    return response
427

    
428
                messages.add_message(request, status, message)
429
                return HttpResponseRedirect(reverse(on_success))
430
            except Exception, e:
431
                if not commited:
432
                    transaction.rollback()
433
                raise
434

    
435
    return render_response(template_name,
436
                           signup_form=form,
437
                           third_party_token=third_party_token,
438
                           provider=provider,
439
                           context_instance=get_context(request, extra_context))
440

    
441

    
442
@require_http_methods(["GET", "POST"])
443
@required_auth_methods_assigned(allow_access=True)
444
@login_required
445
@cookie_fix
446
@signed_terms_required
447
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
448
    """
449
    Allows a user to send feedback.
450

451
    In case of GET request renders a form for providing the feedback information.
452
    In case of POST sends an email to support team.
453

454
    If the user isn't logged in, redirects to settings.LOGIN_URL.
455

456
    **Arguments**
457

458
    ``template_name``
459
        A custom template to use. This is optional; if not specified,
460
        this will default to ``im/feedback.html``.
461

462
    ``extra_context``
463
        An dictionary of variables to add to the template context.
464

465
    **Template:**
466

467
    im/signup.html or ``template_name`` keyword argument.
468

469
    **Settings:**
470

471
    * LOGIN_URL: login uri
472
    """
473
    extra_context = extra_context or {}
474
    if request.method == 'GET':
475
        form = FeedbackForm()
476
    if request.method == 'POST':
477
        if not request.user:
478
            return HttpResponse('Unauthorized', status=401)
479

    
480
        form = FeedbackForm(request.POST)
481
        if form.is_valid():
482
            msg = form.cleaned_data['feedback_msg']
483
            data = form.cleaned_data['feedback_data']
484
            send_feedback(msg, data, request.user, email_template_name)
485
            message = _(astakos_messages.FEEDBACK_SENT)
486
            messages.success(request, message)
487
            return HttpResponseRedirect(reverse('feedback'))
488

    
489
    return render_response(template_name,
490
                           feedback_form=form,
491
                           context_instance=get_context(request,
492
                                                        extra_context))
493

    
494

    
495
@require_http_methods(["GET"])
496
@cookie_fix
497
def logout(request, template='registration/logged_out.html',
498
           extra_context=None):
499
    """
500
    Wraps `django.contrib.auth.logout`.
501
    """
502
    extra_context = extra_context or {}
503
    response = HttpResponse()
504
    if request.user.is_authenticated():
505
        email = request.user.email
506
        auth_logout(request)
507
    else:
508
        response['Location'] = reverse('index')
509
        response.status_code = 301
510
        return response
511

    
512
    next = restrict_next(
513
        request.GET.get('next'),
514
        domain=settings.COOKIE_DOMAIN
515
    )
516

    
517
    if next:
518
        response['Location'] = next
519
        response.status_code = 302
520
    elif settings.LOGOUT_NEXT:
521
        response['Location'] = settings.LOGOUT_NEXT
522
        response.status_code = 301
523
    else:
524
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
525
        provider = auth.get_provider(last_provider)
526
        message = provider.get_logout_success_msg
527
        extra = provider.get_logout_success_extra_msg
528
        if extra:
529
            message += "<br />"  + extra
530
        messages.success(request, message)
531
        response['Location'] = reverse('index')
532
        response.status_code = 301
533
    return response
534

    
535

    
536
@require_http_methods(["GET", "POST"])
537
@cookie_fix
538
@transaction.commit_manually
539
def activate(request, greeting_email_template_name='im/welcome_email.txt',
540
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
541
    """
542
    Activates the user identified by the ``auth`` request parameter, sends a
543
    welcome email and renews the user token.
544

545
    The view uses commit_manually decorator in order to ensure the user state
546
    will be updated only if the email will be send successfully.
547
    """
548
    token = request.GET.get('auth')
549
    next = request.GET.get('next')
550

    
551
    if request.user.is_authenticated():
552
        message = _(astakos_messages.LOGGED_IN_WARNING)
553
        messages.error(request, message)
554
        return HttpResponseRedirect(reverse('index'))
555

    
556
    try:
557
        user = AstakosUser.objects.get(verification_code=token)
558
    except AstakosUser.DoesNotExist:
559
        raise Http404
560

    
561
    if user.email_verified:
562
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
563
        messages.error(request, message)
564
        return HttpResponseRedirect(reverse('index'))
565

    
566
    try:
567
        backend = activation_backends.get_backend()
568
        result = backend.handle_verification(user, token)
569
        backend.send_result_notifications(result, user)
570
        next = settings.ACTIVATION_REDIRECT_URL or next
571
        response = HttpResponseRedirect(reverse('index'))
572
        if user.is_active:
573
            response = prepare_response(request, user, next, renew=True)
574
            messages.success(request, _(result.message))
575
        else:
576
            messages.warning(request, _(result.message))
577
    except Exception:
578
        transaction.rollback()
579
        raise
580
    else:
581
        transaction.commit()
582
        return response
583

    
584

    
585
@require_http_methods(["GET", "POST"])
586
@cookie_fix
587
def approval_terms(request, term_id=None,
588
                   template_name='im/approval_terms.html', extra_context=None):
589
    extra_context = extra_context or {}
590
    term = None
591
    terms = None
592
    if not term_id:
593
        try:
594
            term = ApprovalTerms.objects.order_by('-id')[0]
595
        except IndexError:
596
            pass
597
    else:
598
        try:
599
            term = ApprovalTerms.objects.get(id=term_id)
600
        except ApprovalTerms.DoesNotExist, e:
601
            pass
602

    
603
    if not term:
604
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
605
        return HttpResponseRedirect(reverse('index'))
606
    try:
607
        f = open(term.location, 'r')
608
    except IOError:
609
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
610
        return render_response(
611
            template_name, context_instance=get_context(request,
612
                                                        extra_context))
613

    
614
    terms = f.read()
615

    
616
    if request.method == 'POST':
617
        next = restrict_next(
618
            request.POST.get('next'),
619
            domain=settings.COOKIE_DOMAIN
620
        )
621
        if not next:
622
            next = reverse('index')
623
        form = SignApprovalTermsForm(request.POST, instance=request.user)
624
        if not form.is_valid():
625
            return render_response(template_name,
626
                                   terms=terms,
627
                                   approval_terms_form=form,
628
                                   context_instance=get_context(request,
629
                                                                extra_context))
630
        user = form.save()
631
        return HttpResponseRedirect(next)
632
    else:
633
        form = None
634
        if request.user.is_authenticated() and not request.user.signed_terms:
635
            form = SignApprovalTermsForm(instance=request.user)
636
        return render_response(template_name,
637
                               terms=terms,
638
                               approval_terms_form=form,
639
                               context_instance=get_context(request,
640
                                                            extra_context))
641

    
642

    
643
@require_http_methods(["GET", "POST"])
644
@cookie_fix
645
@transaction.commit_manually
646
def change_email(request, activation_key=None,
647
                 email_template_name='registration/email_change_email.txt',
648
                 form_template_name='registration/email_change_form.html',
649
                 confirm_template_name='registration/email_change_done.html',
650
                 extra_context=None):
651
    extra_context = extra_context or {}
652

    
653
    if not settings.EMAILCHANGE_ENABLED:
654
        raise PermissionDenied
655

    
656
    if activation_key:
657
        try:
658
            try:
659
                email_change = EmailChange.objects.get(
660
                    activation_key=activation_key)
661
            except EmailChange.DoesNotExist:
662
                transaction.rollback()
663
                logger.error("[change-email] Invalid or used activation "
664
                             "code, %s", activation_key)
665
                raise Http404
666

    
667
            if (request.user.is_authenticated() and \
668
                request.user == email_change.user) or not \
669
                    request.user.is_authenticated():
670
                user = EmailChange.objects.change_email(activation_key)
671
                msg = _(astakos_messages.EMAIL_CHANGED)
672
                messages.success(request, msg)
673
                transaction.commit()
674
                return HttpResponseRedirect(reverse('edit_profile'))
675
            else:
676
                logger.error("[change-email] Access from invalid user, %s %s",
677
                             email_change.user, request.user.log_display)
678
                transaction.rollback()
679
                raise PermissionDenied
680
        except ValueError, e:
681
            messages.error(request, e)
682
            transaction.rollback()
683
            return HttpResponseRedirect(reverse('index'))
684

    
685
        return render_response(confirm_template_name,
686
                               modified_user=user if 'user' in locals()
687
                               else None, context_instance=get_context(request,
688
                               extra_context))
689

    
690
    if not request.user.is_authenticated():
691
        path = quote(request.get_full_path())
692
        url = request.build_absolute_uri(reverse('index'))
693
        return HttpResponseRedirect(url + '?next=' + path)
694

    
695
    # clean up expired email changes
696
    if request.user.email_change_is_pending():
697
        change = request.user.emailchanges.get()
698
        if change.activation_key_expired():
699
            change.delete()
700
            transaction.commit()
701
            return HttpResponseRedirect(reverse('email_change'))
702

    
703
    form = EmailChangeForm(request.POST or None)
704
    if request.method == 'POST' and form.is_valid():
705
        try:
706
            ec = form.save(request, email_template_name, request)
707
        except Exception, e:
708
            transaction.rollback()
709
            raise
710
        else:
711
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
712
            messages.success(request, msg)
713
            transaction.commit()
714
            return HttpResponseRedirect(reverse('edit_profile'))
715

    
716
    if request.user.email_change_is_pending():
717
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
718

    
719
    return render_response(
720
        form_template_name,
721
        form=form,
722
        context_instance=get_context(request, extra_context)
723
    )
724

    
725

    
726
@cookie_fix
727
def send_activation(request, user_id, template_name='im/login.html',
728
                    extra_context=None):
729

    
730
    if request.user.is_authenticated():
731
        return HttpResponseRedirect(reverse('index'))
732

    
733
    extra_context = extra_context or {}
734
    try:
735
        u = AstakosUser.objects.get(id=user_id)
736
    except AstakosUser.DoesNotExist:
737
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
738
    else:
739
        if u.email_verified:
740
            logger.warning("[resend activation] Account already verified: %s",
741
                           u.log_display)
742

    
743
            messages.error(request,
744
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
745
        else:
746
            activation_backend = activation_backends.get_backend()
747
            activation_backend.send_user_verification_email(u)
748
            messages.success(request, astakos_messages.ACTIVATION_SENT)
749

    
750
    return HttpResponseRedirect(reverse('index'))
751

    
752

    
753
@require_http_methods(["GET"])
754
@cookie_fix
755
@valid_astakos_user_required
756
def resource_usage(request):
757

    
758
    resources_meta = presentation.RESOURCES
759

    
760
    current_usage = quotas.get_user_quotas(request.user)
761
    current_usage = json.dumps(current_usage['system'])
762
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
763
    if resource_catalog is False:
764
        # on fail resource_groups contains the result object
765
        result = resource_groups
766
        messages.error(request, 'Unable to retrieve system resources: %s' %
767
                       result.reason)
768

    
769
    resource_catalog = json.dumps(resource_catalog)
770
    resource_groups = json.dumps(resource_groups)
771
    resources_order = json.dumps(resources_meta.get('resources_order'))
772

    
773
    return render_response('im/resource_usage.html',
774
                           context_instance=get_context(request),
775
                           resource_catalog=resource_catalog,
776
                           resource_groups=resource_groups,
777
                           resources_order=resources_order,
778
                           current_usage=current_usage,
779
                           token_cookie_name=settings.COOKIE_NAME,
780
                           usage_update_interval=
781
                           settings.USAGE_UPDATE_INTERVAL)
782

    
783

    
784
# TODO: action only on POST and user should confirm the removal
785
@require_http_methods(["GET", "POST"])
786
@cookie_fix
787
@valid_astakos_user_required
788
def remove_auth_provider(request, pk):
789
    try:
790
        provider = request.user.auth_providers.get(pk=int(pk)).settings
791
    except AstakosUserAuthProvider.DoesNotExist:
792
        raise Http404
793

    
794
    if provider.get_remove_policy:
795
        messages.success(request, provider.get_removed_msg)
796
        provider.remove_from_user()
797
        return HttpResponseRedirect(reverse('edit_profile'))
798
    else:
799
        raise PermissionDenied
800

    
801

    
802
@require_http_methods(["GET"])
803
@required_auth_methods_assigned(allow_access=True)
804
@login_required
805
@cookie_fix
806
@signed_terms_required
807
def landing(request):
808
    context = {'services': Service.catalog(orderfor='dashboard')}
809
    return render_response(
810
        'im/landing.html',
811
        context_instance=get_context(request), **context)
812

    
813

    
814
def api_access(request):
815
    return render_response(
816
        'im/api_access.html',
817
        context_instance=get_context(request))
818

    
819
@cookie_fix
820
def get_menu(request, with_extra_links=False, with_signout=True):
821
    user = request.user
822
    index_url = reverse('index')
823

    
824
    if isinstance(user, User) and user.is_authenticated():
825
        l = []
826
        append = l.append
827
        item = MenuItem
828
        item.current_path = request.build_absolute_uri(request.path)
829
        append(item(url=request.build_absolute_uri(reverse('index')),
830
                    name=user.email))
831
        if with_extra_links:
832
            append(item(url=request.build_absolute_uri(reverse('landing')),
833
                        name="Overview"))
834
        if with_signout:
835
            append(item(url=request.build_absolute_uri(reverse('landing')),
836
                        name="Dashboard"))
837
        if with_extra_links:
838
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
839
                        name="Profile"))
840

    
841
        if with_extra_links:
842
            if settings.INVITATIONS_ENABLED:
843
                append(item(url=request.build_absolute_uri(reverse('invite')),
844
                            name="Invitations"))
845

    
846
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
847
                        name="Usage"))
848

    
849
            if settings.PROJECTS_VISIBLE:
850
                append(item(url=request.build_absolute_uri(reverse('project_list')),
851
                            name="Projects"))
852

    
853
            append(item(url=request.build_absolute_uri(reverse('feedback')),
854
                        name="Contact"))
855
        if with_signout:
856
            append(item(url=request.build_absolute_uri(reverse('logout')),
857
                        name="Sign out"))
858
    else:
859
        l = [{'url': request.build_absolute_uri(index_url),
860
              'name': _("Sign in")}]
861

    
862
    callback = request.GET.get('callback', None)
863
    data = json.dumps(tuple(l))
864
    mimetype = 'application/json'
865

    
866
    if callback:
867
        mimetype = 'application/javascript'
868
        data = '%s(%s)' % (callback, data)
869

    
870
    return HttpResponse(content=data, mimetype=mimetype)
871

    
872

    
873
class MenuItem(dict):
874
    current_path = ''
875

    
876
    def __init__(self, *args, **kwargs):
877
        super(MenuItem, self).__init__(*args, **kwargs)
878
        if kwargs.get('url') or kwargs.get('submenu'):
879
            self.__set_is_active__()
880

    
881
    def __setitem__(self, key, value):
882
        super(MenuItem, self).__setitem__(key, value)
883
        if key in ('url', 'submenu'):
884
            self.__set_is_active__()
885

    
886
    def __set_is_active__(self):
887
        if self.get('is_active'):
888
            return
889
        if self.current_path.startswith(self.get('url')):
890
            self.__setitem__('is_active', True)
891
        else:
892
            submenu = self.get('submenu', ())
893
            current = (i for i in submenu if i.get('url') == self.current_path)
894
            try:
895
                current_node = current.next()
896
                if not current_node.get('is_active'):
897
                    current_node.__setitem__('is_active', True)
898
                self.__setitem__('is_active', True)
899
            except StopIteration:
900
                return
901

    
902
    def __setattribute__(self, name, value):
903
        super(MenuItem, self).__setattribute__(name, value)
904
        if name == 'current_path':
905
            self.__set_is_active__()
906

    
907

    
908
def get_services(request):
909
    callback = request.GET.get('callback', None)
910
    mimetype = 'application/json'
911
    data = json.dumps(Service.catalog().values())
912

    
913
    if callback:
914
        # Consume session messages. When get_services is loaded from an astakos
915
        # page, messages should have already been consumed in the html
916
        # response. When get_services is loaded from another domain/service we
917
        # consume them here so that no stale messages to appear if user visits
918
        # an astakos view later on.
919
        # TODO: messages could be served to other services/sites in the dict
920
        # response of get_services and/or get_menu. Services could handle those
921
        # messages respectively.
922
        messages_list = list(messages.get_messages(request))
923
        mimetype = 'application/javascript'
924
        data = '%s(%s)' % (callback, data)
925

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