Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 1ecda536

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 login(request, template_name='im/login.html', extra_context=None):
83
    """
84
    Renders login page.
85

86
    **Arguments**
87

88
    ``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
    ``extra_context``
93
        An dictionary of variables to add to the template context.
94
    """
95

    
96
    extra_context = extra_context or {}
97

    
98
    third_party_token = request.GET.get('key', False)
99
    if third_party_token:
100
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
101

    
102
    if request.user.is_authenticated():
103
        return HttpResponseRedirect(reverse('landing'))
104

    
105
    return render_response(
106
        template_name,
107
        login_form=LoginForm(request=request),
108
        context_instance=get_context(request, extra_context)
109
    )
110

    
111

    
112
@require_http_methods(["GET", "POST"])
113
@cookie_fix
114
@signed_terms_required
115
def index(request, authenticated_redirect='landing',
116
          anonymous_redirect='login', extra_context=None):
117
    """
118
    If user is authenticated redirect to ``authenticated_redirect`` url.
119
    Otherwise redirects to ``anonymous_redirect`` url.
120

121
    """
122
    if request.user.is_authenticated():
123
        return HttpResponseRedirect(reverse(authenticated_redirect))
124
    return HttpResponseRedirect(reverse(anonymous_redirect))
125

    
126

    
127
@require_http_methods(["POST"])
128
@cookie_fix
129
@valid_astakos_user_required
130
def update_token(request):
131
    """
132
    Update api token view.
133
    """
134
    user = request.user
135
    user.renew_token()
136
    user.save()
137
    messages.success(request, astakos_messages.TOKEN_UPDATED)
138
    return HttpResponseRedirect(reverse('api_access'))
139

    
140

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

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

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

156
    If the user isn't logged in, redirects to settings.LOGIN_URL.
157

158
    **Arguments**
159

160
    ``template_name``
161
        A custom template to use. This is optional; if not specified,
162
        this will default to ``im/invitations.html``.
163

164
    ``extra_context``
165
        An dictionary of variables to add to the template context.
166

167
    **Template:**
168

169
    im/invitations.html or ``template_name`` keyword argument.
170

171
    **Settings:**
172

173
    The view expectes the following settings are defined:
174

175
    * LOGIN_URL: login uri
176
    """
177
    extra_context = extra_context or {}
178
    status = None
179
    message = None
180
    form = InvitationForm()
181

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

    
202
    sent = [{'email': inv.username,
203
             'realname': inv.realname,
204
             'is_consumed': inv.is_consumed}
205
            for inv in request.user.invitations_sent.all()]
206
    kwargs = {'inviter': inviter,
207
              'sent': sent}
208
    context = get_context(request, extra_context, **kwargs)
209
    return render_response(template_name,
210
                           invitation_form=form,
211
                           context_instance=context)
212

    
213

    
214
@require_http_methods(["GET", "POST"])
215
@required_auth_methods_assigned()
216
@login_required
217
@cookie_fix
218
@signed_terms_required
219
def api_access(request, template_name='im/api_access.html',
220
               extra_context=None):
221
    """
222
    API access view.
223
    """
224
    context = {}
225

    
226
    token_url = join_urls(settings.BASE_HOST, reverse('tokens_authenticate'))
227
    context['services'] = Component.catalog()
228
    context['token_url'] = token_url
229
    context['client_url'] = settings.API_CLIENT_URL
230

    
231
    if extra_context:
232
        context.update(extra_context)
233
    context_instance = get_context(request, context)
234
    return render_response(template_name,
235
                           context_instance=context_instance)
236

    
237

    
238
@require_http_methods(["GET", "POST"])
239
@required_auth_methods_assigned(allow_access=True)
240
@login_required
241
@cookie_fix
242
@signed_terms_required
243
def edit_profile(request, template_name='im/profile.html', extra_context=None):
244
    """
245
    Allows a user to edit his/her profile.
246

247
    In case of GET request renders a form for displaying the user information.
248
    In case of POST updates the user informantion and redirects to ``next``
249
    url parameter if exists.
250

251
    If the user isn't logged in, redirects to settings.LOGIN_URL.
252

253
    **Arguments**
254

255
    ``template_name``
256
        A custom template to use. This is optional; if not specified,
257
        this will default to ``im/profile.html``.
258

259
    ``extra_context``
260
        An dictionary of variables to add to the template context.
261

262
    **Template:**
263

264
    im/profile.html or ``template_name`` keyword argument.
265

266
    **Settings:**
267

268
    The view expectes the following settings are defined:
269

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

    
295
                if form.email_changed:
296
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
297
                    messages.success(request, msg)
298
                if form.password_changed:
299
                    msg = _(astakos_messages.PASSWORD_CHANGED)
300
                    messages.success(request, msg)
301

    
302
                if next:
303
                    return redirect(next)
304
                else:
305
                    return redirect(reverse('edit_profile'))
306
            except ValueError, ve:
307
                messages.success(request, ve)
308
    elif request.method == "GET":
309
        request.user.is_verified = True
310
        request.user.save()
311

    
312
    # existing providers
313
    user_providers = request.user.get_enabled_auth_providers()
314
    user_disabled_providers = request.user.get_disabled_auth_providers()
315

    
316
    # providers that user can add
317
    user_available_providers = request.user.get_available_auth_providers()
318

    
319
    extra_context['services'] = Component.catalog().values()
320
    return render_response(template_name,
321
                           profile_form=form,
322
                           user_providers=user_providers,
323
                           user_disabled_providers=user_disabled_providers,
324
                           user_available_providers=user_available_providers,
325
                           context_instance=get_context(request,
326
                                                          extra_context))
327

    
328

    
329
@transaction.commit_manually
330
@require_http_methods(["GET", "POST"])
331
@cookie_fix
332
def signup(request, template_name='im/signup.html', on_success='index',
333
           extra_context=None, activation_backend=None):
334
    """
335
    Allows a user to create a local account.
336

337
    In case of GET request renders a form for entering the user information.
338
    In case of POST handles the signup.
339

340
    The user activation will be delegated to the backend specified by the
341
    ``activation_backend`` keyword argument if present, otherwise to the
342
    ``astakos.im.activation_backends.InvitationBackend`` if
343
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
344
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
345
    activation_backends);
346

347
    Upon successful user creation, if ``next`` url parameter is present the
348
    user is redirected there otherwise renders the same page with a success
349
    message.
350

351
    On unsuccessful creation, renders ``template_name`` with an error message.
352

353
    **Arguments**
354

355
    ``template_name``
356
        A custom template to render. This is optional;
357
        if not specified, this will default to ``im/signup.html``.
358

359
    ``extra_context``
360
        An dictionary of variables to add to the template context.
361

362
    ``on_success``
363
        Resolvable view name to redirect on registration success.
364

365
    **Template:**
366

367
    im/signup.html or ``template_name`` keyword argument.
368
    """
369
    extra_context = extra_context or {}
370
    if request.user.is_authenticated():
371
        logger.info("%s already signed in, redirect to index",
372
                    request.user.log_display)
373
        return HttpResponseRedirect(reverse('index'))
374

    
375
    provider = get_query(request).get('provider', 'local')
376
    if not auth.get_provider(provider).get_create_policy:
377
        logger.error("%s provider not available for signup", provider)
378
        raise PermissionDenied
379

    
380
    instance = None
381

    
382
    # user registered using third party provider
383
    third_party_token = request.REQUEST.get('third_party_token', None)
384
    unverified = None
385
    if third_party_token:
386
        # retreive third party entry. This was created right after the initial
387
        # third party provider handshake.
388
        pending = get_object_or_404(PendingThirdPartyUser,
389
                                    token=third_party_token)
390

    
391
        provider = pending.provider
392

    
393
        # clone third party instance into the corresponding AstakosUser
394
        instance = pending.get_user_instance()
395
        get_unverified = AstakosUserAuthProvider.objects.unverified
396

    
397
        # check existing unverified entries
398
        unverified = get_unverified(pending.provider,
399
                                    identifier=pending.third_party_identifier)
400

    
401
        if unverified and request.method == 'GET':
402
            messages.warning(request, unverified.get_pending_registration_msg)
403
            if unverified.user.moderated:
404
                messages.warning(request,
405
                                 unverified.get_pending_resend_activation_msg)
406
            else:
407
                messages.warning(request,
408
                                 unverified.get_pending_moderation_msg)
409

    
410
    # prepare activation backend based on current request
411
    if not activation_backend:
412
        activation_backend = activation_backends.get_backend()
413

    
414
    form_kwargs = {'instance': instance}
415
    if third_party_token:
416
        form_kwargs['third_party_token'] = third_party_token
417

    
418
    form = activation_backend.get_signup_form(
419
        provider, None, **form_kwargs)
420

    
421
    if request.method == 'POST':
422
        form = activation_backend.get_signup_form(
423
            provider,
424
            request.POST,
425
            **form_kwargs)
426

    
427
        if form.is_valid():
428
            commited = False
429
            try:
430
                user = form.save(commit=False)
431

    
432
                # delete previously unverified accounts
433
                if AstakosUser.objects.user_exists(user.email):
434
                    AstakosUser.objects.get_by_identifier(user.email).delete()
435

    
436
                # store_user so that user auth providers get initialized
437
                form.store_user(user, request)
438
                result = activation_backend.handle_registration(user)
439
                if result.status == \
440
                        activation_backend.Result.PENDING_MODERATION:
441
                    # user should be warned that his account is not active yet
442
                    status = messages.WARNING
443
                else:
444
                    status = messages.SUCCESS
445
                message = result.message
446
                activation_backend.send_result_notifications(result, user)
447

    
448
                # commit user entry
449
                transaction.commit()
450
                # commited flag
451
                # in case an exception get raised from this point
452
                commited = True
453

    
454
                if user and user.is_active:
455
                    # activation backend directly activated the user
456
                    # log him in
457
                    next = request.POST.get('next', '')
458
                    response = prepare_response(request, user, next=next)
459
                    return response
460

    
461
                messages.add_message(request, status, message)
462
                return HttpResponseRedirect(reverse(on_success))
463
            except Exception, e:
464
                if not commited:
465
                    transaction.rollback()
466
                raise
467

    
468
    return render_response(template_name,
469
                           signup_form=form,
470
                           third_party_token=third_party_token,
471
                           provider=provider,
472
                           context_instance=get_context(request, extra_context))
473

    
474

    
475
@require_http_methods(["GET", "POST"])
476
@required_auth_methods_assigned(allow_access=True)
477
@login_required
478
@cookie_fix
479
@signed_terms_required
480
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
481
    """
482
    Allows a user to send feedback.
483

484
    In case of GET request renders a form for providing the feedback information.
485
    In case of POST sends an email to support team.
486

487
    If the user isn't logged in, redirects to settings.LOGIN_URL.
488

489
    **Arguments**
490

491
    ``template_name``
492
        A custom template to use. This is optional; if not specified,
493
        this will default to ``im/feedback.html``.
494

495
    ``extra_context``
496
        An dictionary of variables to add to the template context.
497

498
    **Template:**
499

500
    im/signup.html or ``template_name`` keyword argument.
501

502
    **Settings:**
503

504
    * LOGIN_URL: login uri
505
    """
506
    extra_context = extra_context or {}
507
    if request.method == 'GET':
508
        form = FeedbackForm()
509
    if request.method == 'POST':
510
        if not request.user:
511
            return HttpResponse('Unauthorized', status=401)
512

    
513
        form = FeedbackForm(request.POST)
514
        if form.is_valid():
515
            msg = form.cleaned_data['feedback_msg']
516
            data = form.cleaned_data['feedback_data']
517
            send_feedback(msg, data, request.user, email_template_name)
518
            message = _(astakos_messages.FEEDBACK_SENT)
519
            messages.success(request, message)
520
            return HttpResponseRedirect(reverse('feedback'))
521

    
522
    return render_response(template_name,
523
                           feedback_form=form,
524
                           context_instance=get_context(request,
525
                                                        extra_context))
526

    
527

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

    
545
    next = restrict_next(
546
        request.GET.get('next'),
547
        domain=settings.COOKIE_DOMAIN
548
    )
549

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

    
568

    
569
@require_http_methods(["GET", "POST"])
570
@cookie_fix
571
@transaction.commit_manually
572
def activate(request, greeting_email_template_name='im/welcome_email.txt',
573
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
574
    """
575
    Activates the user identified by the ``auth`` request parameter, sends a
576
    welcome email and renews the user token.
577

578
    The view uses commit_manually decorator in order to ensure the user state
579
    will be updated only if the email will be send successfully.
580
    """
581
    token = request.GET.get('auth')
582
    next = request.GET.get('next')
583

    
584
    if request.user.is_authenticated():
585
        message = _(astakos_messages.LOGGED_IN_WARNING)
586
        messages.error(request, message)
587
        return HttpResponseRedirect(reverse('index'))
588

    
589
    try:
590
        user = AstakosUser.objects.get(verification_code=token)
591
    except AstakosUser.DoesNotExist:
592
        raise Http404
593

    
594
    if user.email_verified:
595
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
596
        messages.error(request, message)
597
        return HttpResponseRedirect(reverse('index'))
598

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

    
617

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

    
636
    if not term:
637
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
638
        return HttpResponseRedirect(reverse('index'))
639
    try:
640
        f = open(term.location, 'r')
641
    except IOError:
642
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
643
        return render_response(
644
            template_name, context_instance=get_context(request,
645
                                                        extra_context))
646

    
647
    terms = f.read()
648

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

    
675

    
676
@require_http_methods(["GET", "POST"])
677
@cookie_fix
678
@transaction.commit_manually
679
def change_email(request, activation_key=None,
680
                 email_template_name='registration/email_change_email.txt',
681
                 form_template_name='registration/email_change_form.html',
682
                 confirm_template_name='registration/email_change_done.html',
683
                 extra_context=None):
684
    extra_context = extra_context or {}
685

    
686
    if not settings.EMAILCHANGE_ENABLED:
687
        raise PermissionDenied
688

    
689
    if activation_key:
690
        try:
691
            try:
692
                email_change = EmailChange.objects.get(
693
                    activation_key=activation_key)
694
            except EmailChange.DoesNotExist:
695
                transaction.rollback()
696
                logger.error("[change-email] Invalid or used activation "
697
                             "code, %s", activation_key)
698
                raise Http404
699

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

    
718
        return render_response(confirm_template_name,
719
                               modified_user=user if 'user' in locals()
720
                               else None, context_instance=get_context(request,
721
                               extra_context))
722

    
723
    if not request.user.is_authenticated():
724
        path = quote(request.get_full_path())
725
        url = request.build_absolute_uri(reverse('index'))
726
        return HttpResponseRedirect(url + '?next=' + path)
727

    
728
    # clean up expired email changes
729
    if request.user.email_change_is_pending():
730
        change = request.user.emailchanges.get()
731
        if change.activation_key_expired():
732
            change.delete()
733
            transaction.commit()
734
            return HttpResponseRedirect(reverse('email_change'))
735

    
736
    form = EmailChangeForm(request.POST or None)
737
    if request.method == 'POST' and form.is_valid():
738
        try:
739
            ec = form.save(request, email_template_name, request)
740
        except Exception, e:
741
            transaction.rollback()
742
            raise
743
        else:
744
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
745
            messages.success(request, msg)
746
            transaction.commit()
747
            return HttpResponseRedirect(reverse('edit_profile'))
748

    
749
    if request.user.email_change_is_pending():
750
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
751

    
752
    return render_response(
753
        form_template_name,
754
        form=form,
755
        context_instance=get_context(request, extra_context)
756
    )
757

    
758

    
759
@cookie_fix
760
def send_activation(request, user_id, template_name='im/login.html',
761
                    extra_context=None):
762

    
763
    if request.user.is_authenticated():
764
        return HttpResponseRedirect(reverse('index'))
765

    
766
    extra_context = extra_context or {}
767
    try:
768
        u = AstakosUser.objects.get(id=user_id)
769
    except AstakosUser.DoesNotExist:
770
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
771
    else:
772
        if u.email_verified:
773
            logger.warning("[resend activation] Account already verified: %s",
774
                           u.log_display)
775

    
776
            messages.error(request,
777
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
778
        else:
779
            activation_backend = activation_backends.get_backend()
780
            activation_backend.send_user_verification_email(u)
781
            messages.success(request, astakos_messages.ACTIVATION_SENT)
782

    
783
    return HttpResponseRedirect(reverse('index'))
784

    
785

    
786
@require_http_methods(["GET"])
787
@cookie_fix
788
@valid_astakos_user_required
789
def resource_usage(request):
790

    
791
    resources_meta = presentation.RESOURCES
792

    
793
    current_usage = quotas.get_user_quotas(request.user)
794
    current_usage = json.dumps(current_usage['system'])
795
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
796
    if resource_catalog is False:
797
        # on fail resource_groups contains the result object
798
        result = resource_groups
799
        messages.error(request, 'Unable to retrieve system resources: %s' %
800
                       result.reason)
801

    
802
    resource_catalog = json.dumps(resource_catalog)
803
    resource_groups = json.dumps(resource_groups)
804
    resources_order = json.dumps(resources_meta.get('resources_order'))
805

    
806
    return render_response('im/resource_usage.html',
807
                           context_instance=get_context(request),
808
                           resource_catalog=resource_catalog,
809
                           resource_groups=resource_groups,
810
                           resources_order=resources_order,
811
                           current_usage=current_usage,
812
                           token_cookie_name=settings.COOKIE_NAME,
813
                           usage_update_interval=
814
                           settings.USAGE_UPDATE_INTERVAL)
815

    
816

    
817
# TODO: action only on POST and user should confirm the removal
818
@require_http_methods(["POST"])
819
@cookie_fix
820
@valid_astakos_user_required
821
def remove_auth_provider(request, pk):
822
    try:
823
        provider = request.user.auth_providers.get(pk=int(pk)).settings
824
    except AstakosUserAuthProvider.DoesNotExist:
825
        raise Http404
826

    
827
    if provider.get_remove_policy:
828
        messages.success(request, provider.get_removed_msg)
829
        provider.remove_from_user()
830
        return HttpResponseRedirect(reverse('edit_profile'))
831
    else:
832
        raise PermissionDenied
833

    
834

    
835
@require_http_methods(["GET"])
836
@required_auth_methods_assigned(allow_access=True)
837
@login_required
838
@cookie_fix
839
@signed_terms_required
840
def landing(request):
841
    context = {'services': Component.catalog(orderfor='dashboard')}
842
    return render_response(
843
        'im/landing.html',
844
        context_instance=get_context(request), **context)
845

    
846

    
847
@cookie_fix
848
def get_menu(request, with_extra_links=False, with_signout=True):
849
    user = request.user
850
    index_url = reverse('index')
851

    
852
    if isinstance(user, User) and user.is_authenticated():
853
        l = []
854
        append = l.append
855
        item = MenuItem
856
        item.current_path = request.build_absolute_uri(request.path)
857
        append(item(url=request.build_absolute_uri(reverse('index')),
858
                    name=user.email))
859
        if with_extra_links:
860
            append(item(url=request.build_absolute_uri(reverse('landing')),
861
                        name="Overview"))
862
        if with_signout:
863
            append(item(url=request.build_absolute_uri(reverse('landing')),
864
                        name="Dashboard"))
865
        if with_extra_links:
866
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
867
                        name="Profile"))
868

    
869
        if with_extra_links:
870
            if settings.INVITATIONS_ENABLED:
871
                append(item(url=request.build_absolute_uri(reverse('invite')),
872
                            name="Invitations"))
873

    
874
            append(item(url=request.build_absolute_uri(reverse('api_access')),
875
                        name="API access"))
876

    
877
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
878
                        name="Usage"))
879

    
880
            if settings.PROJECTS_VISIBLE:
881
                append(item(url=request.build_absolute_uri(reverse('project_list')),
882
                            name="Projects"))
883

    
884
            append(item(url=request.build_absolute_uri(reverse('feedback')),
885
                        name="Contact"))
886
        if with_signout:
887
            append(item(url=request.build_absolute_uri(reverse('logout')),
888
                        name="Sign out"))
889
    else:
890
        l = [{'url': request.build_absolute_uri(index_url),
891
              'name': _("Sign in")}]
892

    
893
    callback = request.GET.get('callback', None)
894
    data = json.dumps(tuple(l))
895
    mimetype = 'application/json'
896

    
897
    if callback:
898
        mimetype = 'application/javascript'
899
        data = '%s(%s)' % (callback, data)
900

    
901
    return HttpResponse(content=data, mimetype=mimetype)
902

    
903

    
904
class MenuItem(dict):
905
    current_path = ''
906

    
907
    def __init__(self, *args, **kwargs):
908
        super(MenuItem, self).__init__(*args, **kwargs)
909
        if kwargs.get('url') or kwargs.get('submenu'):
910
            self.__set_is_active__()
911

    
912
    def __setitem__(self, key, value):
913
        super(MenuItem, self).__setitem__(key, value)
914
        if key in ('url', 'submenu'):
915
            self.__set_is_active__()
916

    
917
    def __set_is_active__(self):
918
        if self.get('is_active'):
919
            return
920
        if self.current_path.startswith(self.get('url')):
921
            self.__setitem__('is_active', True)
922
        else:
923
            submenu = self.get('submenu', ())
924
            current = (i for i in submenu if i.get('url') == self.current_path)
925
            try:
926
                current_node = current.next()
927
                if not current_node.get('is_active'):
928
                    current_node.__setitem__('is_active', True)
929
                self.__setitem__('is_active', True)
930
            except StopIteration:
931
                return
932

    
933
    def __setattribute__(self, name, value):
934
        super(MenuItem, self).__setattribute__(name, value)
935
        if name == 'current_path':
936
            self.__set_is_active__()
937

    
938

    
939
def get_services(request):
940
    callback = request.GET.get('callback', None)
941
    mimetype = 'application/json'
942
    data = json.dumps(Component.catalog().values())
943

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

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