Statistics
| Branch: | Tag: | Revision:

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

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_HOST, reverse('tokens_authenticate'))
220
    context['services'] = Component.catalog()
221
    context['token_url'] = token_url
222
    context['client_url'] = settings.API_CLIENT_URL
223

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

    
230

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

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

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

246
    **Arguments**
247

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

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

255
    **Template:**
256

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

259
    **Settings:**
260

261
    The view expectes the following settings are defined:
262

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

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

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

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

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

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

    
321

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

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

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

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

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

346
    **Arguments**
347

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

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

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

358
    **Template:**
359

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

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

    
373
    instance = None
374

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

    
384
        provider = pending.provider
385

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
467

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

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

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

482
    **Arguments**
483

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

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

491
    **Template:**
492

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

495
    **Settings:**
496

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

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

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

    
520

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

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

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

    
561

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

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

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

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

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

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

    
610

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

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

    
640
    terms = f.read()
641

    
642
    if request.method == 'POST':
643
        next = restrict_next(
644
            request.POST.get('next'),
645
            domain=settings.COOKIE_DOMAIN
646
        )
647
        if not next:
648
            next = reverse('index')
649
        form = SignApprovalTermsForm(request.POST, instance=request.user)
650
        if not form.is_valid():
651
            return render_response(template_name,
652
                                   terms=terms,
653
                                   approval_terms_form=form,
654
                                   context_instance=get_context(request,
655
                                                                extra_context))
656
        user = form.save()
657
        return HttpResponseRedirect(next)
658
    else:
659
        form = None
660
        if request.user.is_authenticated() and not request.user.signed_terms:
661
            form = SignApprovalTermsForm(instance=request.user)
662
        return render_response(template_name,
663
                               terms=terms,
664
                               approval_terms_form=form,
665
                               context_instance=get_context(request,
666
                                                            extra_context))
667

    
668

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

    
679
    if not settings.EMAILCHANGE_ENABLED:
680
        raise PermissionDenied
681

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

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

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

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

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

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

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

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

    
751

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

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

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

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

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

    
778

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

    
784
    resources_meta = presentation.RESOURCES
785

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

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

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

    
809

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

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

    
827

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

    
839

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

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

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

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

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

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

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

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

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

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

    
896

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

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

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

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

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

    
931

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

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

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