Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 5bc77346

History | View | Annotate | Download (34.6 kB)

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

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.contrib.auth.models import User
44
from django.core.urlresolvers import reverse
45
from django.db import transaction
46
from django.http import HttpResponse, HttpResponseRedirect, Http404
47
from django.shortcuts import redirect
48
from django.utils.translation import ugettext as _
49
from django.core.exceptions import PermissionDenied
50
from django.views.decorators.http import require_http_methods
51
from django.utils import simplejson as json
52
from django.template import RequestContext
53

    
54
from synnefo_branding import utils as branding
55
from synnefo_branding import settings as branding_settings
56

    
57
import astakos.im.messages as astakos_messages
58

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

    
78
logger = logging.getLogger(__name__)
79

    
80

    
81
@require_http_methods(["GET", "POST"])
82
@cookie_fix
83
@signed_terms_required
84
def login(request, template_name='im/login.html', extra_context=None):
85
    """
86
    Renders login page.
87

88
    **Arguments**
89

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

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

    
98
    extra_context = extra_context or {}
99

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

    
104
    if request.user.is_authenticated():
105
        return HttpResponseRedirect(reverse('landing'))
106

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

    
113

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

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

    
128

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

    
142

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

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

156
    The number of the user invitations is going to be updated only if the email
157
    has been successfully sent.
158

159
    If the user isn't logged in, redirects to settings.LOGIN_URL.
160

161
    **Arguments**
162

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

167
    ``extra_context``
168
        An dictionary of variables to add to the template context.
169

170
    **Template:**
171

172
    im/invitations.html or ``template_name`` keyword argument.
173

174
    **Settings:**
175

176
    The view expectes the following settings are defined:
177

178
    * LOGIN_URL: login uri
179
    """
180
    extra_context = extra_context or {}
181
    status = None
182
    message = None
183
    form = InvitationForm()
184

    
185
    inviter = request.user
186
    if request.method == 'POST':
187
        form = InvitationForm(request.POST)
188
        if inviter.invitations > 0:
189
            if form.is_valid():
190
                email = form.cleaned_data.get('username')
191
                realname = form.cleaned_data.get('realname')
192
                invite_func(inviter, email, realname)
193
                message = _(astakos_messages.INVITATION_SENT) % locals()
194
                messages.success(request, message)
195
        else:
196
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
197
            messages.error(request, message)
198

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

    
210

    
211
@require_http_methods(["GET", "POST"])
212
@required_auth_methods_assigned()
213
@login_required
214
@cookie_fix
215
@signed_terms_required
216
def api_access_config(request, template_name='im/api_access_config.html',
217
                      content_type='text/plain', extra_context=None,
218
                      filename='.kamakirc'):
219

    
220
    if settings.KAMAKI_CONFIG_CLOUD_NAME:
221
        cloud_name = settings.KAMAKI_CONFIG_CLOUD_NAME
222
    else:
223
        cloud_name = branding_settings.SERVICE_NAME.replace(' ', '_').lower()
224

    
225
    url = get_public_endpoint(settings.astakos_services, 'identity')
226

    
227
    context = {
228
        'user': request.user,
229
        'services': Component.catalog(),
230
        'token_url': url,
231
        'cloud_name': cloud_name
232
    }
233

    
234
    extra_context = extra_context or {}
235
    context.update(extra_context)
236
    content = branding.render_to_string(template_name, context,
237
                                        RequestContext(request))
238
    response = HttpResponse(content_type=content_type)
239
    response.status_code = 200
240
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
241
    response.content = content
242
    return response
243

    
244

    
245
@required_auth_methods_assigned()
246
@login_required
247
@cookie_fix
248
@signed_terms_required
249
def api_access(request, template_name='im/api_access.html',
250
               extra_context=None):
251
    """
252
    API access view.
253
    """
254
    context = {}
255

    
256
    url = get_public_endpoint(settings.astakos_services, 'identity')
257
    context['services'] = Component.catalog()
258
    context['token_url'] = url
259
    context['user'] = request.user
260
    context['client_url'] = settings.API_CLIENT_URL
261

    
262
    if extra_context:
263
        context.update(extra_context)
264
    context_instance = get_context(request, context)
265
    return render_response(template_name,
266
                           context_instance=context_instance)
267

    
268

    
269
@require_http_methods(["GET", "POST"])
270
@required_auth_methods_assigned(allow_access=True)
271
@login_required
272
@cookie_fix
273
@signed_terms_required
274
def edit_profile(request, template_name='im/profile.html', extra_context=None):
275
    """
276
    Allows a user to edit his/her profile.
277

278
    In case of GET request renders a form for displaying the user information.
279
    In case of POST updates the user informantion and redirects to ``next``
280
    url parameter if exists.
281

282
    If the user isn't logged in, redirects to settings.LOGIN_URL.
283

284
    **Arguments**
285

286
    ``template_name``
287
        A custom template to use. This is optional; if not specified,
288
        this will default to ``im/profile.html``.
289

290
    ``extra_context``
291
        An dictionary of variables to add to the template context.
292

293
    **Template:**
294

295
    im/profile.html or ``template_name`` keyword argument.
296

297
    **Settings:**
298

299
    The view expectes the following settings are defined:
300

301
    * LOGIN_URL: login uri
302
    """
303
    extra_context = extra_context or {}
304
    form = ProfileForm(
305
        instance=request.user,
306
        session_key=request.session.session_key
307
    )
308
    extra_context['next'] = request.GET.get('next')
309
    if request.method == 'POST':
310
        form = ProfileForm(
311
            request.POST,
312
            instance=request.user,
313
            session_key=request.session.session_key
314
        )
315
        if form.is_valid():
316
            try:
317
                prev_token = request.user.auth_token
318
                user = form.save(request=request)
319
                next = restrict_next(
320
                    request.POST.get('next'),
321
                    domain=settings.COOKIE_DOMAIN
322
                )
323
                msg = _(astakos_messages.PROFILE_UPDATED)
324
                messages.success(request, msg)
325

    
326
                if form.email_changed:
327
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
328
                    messages.success(request, msg)
329
                if form.password_changed:
330
                    msg = _(astakos_messages.PASSWORD_CHANGED)
331
                    messages.success(request, msg)
332

    
333
                if next:
334
                    return redirect(next)
335
                else:
336
                    return redirect(reverse('edit_profile'))
337
            except ValueError, ve:
338
                messages.success(request, ve)
339
    elif request.method == "GET":
340
        request.user.is_verified = True
341
        request.user.save()
342

    
343
    # existing providers
344
    user_providers = request.user.get_enabled_auth_providers()
345
    user_disabled_providers = request.user.get_disabled_auth_providers()
346

    
347
    # providers that user can add
348
    user_available_providers = request.user.get_available_auth_providers()
349

    
350
    extra_context['services'] = Component.catalog().values()
351
    return render_response(template_name,
352
                           profile_form=form,
353
                           user_providers=user_providers,
354
                           user_disabled_providers=user_disabled_providers,
355
                           user_available_providers=user_available_providers,
356
                           context_instance=get_context(request,
357
                                                        extra_context))
358

    
359

    
360
@transaction.commit_on_success
361
@require_http_methods(["GET", "POST"])
362
@cookie_fix
363
def signup(request, template_name='im/signup.html', on_success='index',
364
           extra_context=None, activation_backend=None):
365
    """
366
    Allows a user to create a local account.
367

368
    In case of GET request renders a form for entering the user information.
369
    In case of POST handles the signup.
370

371
    The user activation will be delegated to the backend specified by the
372
    ``activation_backend`` keyword argument if present, otherwise to the
373
    ``astakos.im.activation_backends.InvitationBackend`` if
374
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
375
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
376
    activation_backends);
377

378
    Upon successful user creation, if ``next`` url parameter is present the
379
    user is redirected there otherwise renders the same page with a success
380
    message.
381

382
    On unsuccessful creation, renders ``template_name`` with an error message.
383

384
    **Arguments**
385

386
    ``template_name``
387
        A custom template to render. This is optional;
388
        if not specified, this will default to ``im/signup.html``.
389

390
    ``extra_context``
391
        An dictionary of variables to add to the template context.
392

393
    ``on_success``
394
        Resolvable view name to redirect on registration success.
395

396
    **Template:**
397

398
    im/signup.html or ``template_name`` keyword argument.
399
    """
400
    extra_context = extra_context or {}
401
    if request.user.is_authenticated():
402
        logger.info("%s already signed in, redirect to index",
403
                    request.user.log_display)
404
        return HttpResponseRedirect(reverse('index'))
405

    
406
    provider = get_query(request).get('provider', 'local')
407
    if not auth.get_provider(provider).get_create_policy:
408
        logger.error("%s provider not available for signup", provider)
409
        raise PermissionDenied
410

    
411
    instance = None
412

    
413
    # user registered using third party provider
414
    third_party_token = request.REQUEST.get('third_party_token', None)
415
    unverified = None
416
    pending = None
417
    if third_party_token:
418
        # retreive third party entry. This was created right after the initial
419
        # third party provider handshake.
420
        pending = get_object_or_404(PendingThirdPartyUser,
421
                                    token=third_party_token)
422

    
423
        provider = pending.provider
424

    
425
        # clone third party instance into the corresponding AstakosUser
426
        instance = pending.get_user_instance()
427
        get_unverified = AstakosUserAuthProvider.objects.unverified
428

    
429
        # check existing unverified entries
430
        unverified = get_unverified(pending.provider,
431
                                    identifier=pending.third_party_identifier)
432

    
433
        if unverified and request.method == 'GET':
434
            messages.warning(request, unverified.get_pending_registration_msg)
435
            if unverified.user.moderated:
436
                messages.warning(request,
437
                                 unverified.get_pending_resend_activation_msg)
438
            else:
439
                messages.warning(request,
440
                                 unverified.get_pending_moderation_msg)
441

    
442
    # prepare activation backend based on current request
443
    if not activation_backend:
444
        activation_backend = activation_backends.get_backend()
445

    
446
    form_kwargs = {'instance': instance, 'request': request}
447
    if third_party_token:
448
        form_kwargs['third_party_token'] = third_party_token
449

    
450
    if pending:
451
        form_kwargs['initial'] = {
452
            'first_name': pending.first_name,
453
            'last_name': pending.last_name,
454
            'email': pending.email
455
        }
456

    
457
    form = activation_backend.get_signup_form(
458
        provider, None, **form_kwargs)
459

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

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

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

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

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

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

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

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

    
505

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

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

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

522
    **Arguments**
523

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

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

531
    **Template:**
532

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

535
    **Settings:**
536

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

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

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

    
560

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

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

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

    
602

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

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

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

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

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

    
633
    backend = activation_backends.get_backend()
634
    result = backend.handle_verification(user, token)
635
    backend.send_result_notifications(result, user)
636
    next = settings.ACTIVATION_REDIRECT_URL or next
637
    response = HttpResponseRedirect(reverse('index'))
638
    if user.is_active:
639
        response = prepare_response(request, user, next, renew=True)
640
        messages.success(request, _(result.message))
641
    else:
642
        messages.warning(request, _(result.message))
643

    
644
    return response
645

    
646

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

    
665

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

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

    
695
    terms = f.read()
696

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

    
710

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

    
721
    if not settings.EMAILCHANGE_ENABLED:
722
        raise PermissionDenied
723

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

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

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

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

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

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

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

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

    
790

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

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

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

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

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

    
817

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

    
823
    resources_meta = presentation.RESOURCES
824

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

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

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

    
848

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

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

    
866

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

    
878

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

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

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

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

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

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

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

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

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

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

    
942

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

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

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

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

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

    
977

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

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

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