Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 07ad7b1c

History | View | Annotate | Download (35.3 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
from synnefo.lib import join_urls
58

    
59
import astakos.im.messages as astakos_messages
60

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

    
80
logger = logging.getLogger(__name__)
81

    
82

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

90
    **Arguments**
91

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

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

    
100
    extra_context = extra_context or {}
101

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

    
106
    if request.user.is_authenticated():
107
        return HttpResponseRedirect(reverse('landing'))
108

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

    
115

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

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

    
130

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

    
144

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

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

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

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

162
    **Arguments**
163

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

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

171
    **Template:**
172

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

175
    **Settings:**
176

177
    The view expectes the following settings are defined:
178

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

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

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

    
217

    
218

    
219
@require_http_methods(["GET", "POST"])
220
@required_auth_methods_assigned()
221
@login_required
222
@cookie_fix
223
@signed_terms_required
224
def api_access_config(request, template_name='im/api_access_config.html',
225
                      content_type='text/plain', extra_context=None,
226
                      filename='.kamakirc'):
227

    
228
    if settings.KAMAKI_CONFIG_CLOUD_NAME:
229
        cloud_name = settings.KAMAKI_CONFIG_CLOUD_NAME
230
    else:
231
        cloud_name = branding_settings.SERVICE_NAME.replace(' ', '_').lower()
232

    
233
    url = get_public_endpoint(settings.astakos_services, 'identity')
234

    
235
    context = {
236
        'user': request.user,
237
        'services': Component.catalog(),
238
        'token_url': url,
239
        'cloud_name': cloud_name
240
    }
241

    
242
    extra_context = extra_context or {}
243
    context.update(extra_context)
244
    content = branding.render_to_string(template_name, context,
245
                                        RequestContext(request))
246
    response = HttpResponse(content_type=content_type)
247
    response.status_code = 200
248
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
249
    response.content = content
250
    return response
251

    
252

    
253
@required_auth_methods_assigned()
254
@login_required
255
@cookie_fix
256
@signed_terms_required
257
def api_access(request, template_name='im/api_access.html',
258
               extra_context=None):
259
    """
260
    API access view.
261
    """
262
    context = {}
263

    
264
    url = get_public_endpoint(settings.astakos_services, 'identity')
265
    context['services'] = Component.catalog()
266
    context['token_url'] = url
267
    context['user'] = request.user
268
    context['client_url'] = settings.API_CLIENT_URL
269

    
270
    if extra_context:
271
        context.update(extra_context)
272
    context_instance = get_context(request, context)
273
    return render_response(template_name,
274
                           context_instance=context_instance)
275

    
276

    
277
@require_http_methods(["GET", "POST"])
278
@required_auth_methods_assigned(allow_access=True)
279
@login_required
280
@cookie_fix
281
@signed_terms_required
282
def edit_profile(request, template_name='im/profile.html', extra_context=None):
283
    """
284
    Allows a user to edit his/her profile.
285

286
    In case of GET request renders a form for displaying the user information.
287
    In case of POST updates the user informantion and redirects to ``next``
288
    url parameter if exists.
289

290
    If the user isn't logged in, redirects to settings.LOGIN_URL.
291

292
    **Arguments**
293

294
    ``template_name``
295
        A custom template to use. This is optional; if not specified,
296
        this will default to ``im/profile.html``.
297

298
    ``extra_context``
299
        An dictionary of variables to add to the template context.
300

301
    **Template:**
302

303
    im/profile.html or ``template_name`` keyword argument.
304

305
    **Settings:**
306

307
    The view expectes the following settings are defined:
308

309
    * LOGIN_URL: login uri
310
    """
311
    extra_context = extra_context or {}
312
    form = ProfileForm(
313
        instance=request.user,
314
        session_key=request.session.session_key
315
    )
316
    extra_context['next'] = request.GET.get('next')
317
    if request.method == 'POST':
318
        form = ProfileForm(
319
            request.POST,
320
            instance=request.user,
321
            session_key=request.session.session_key
322
        )
323
        if form.is_valid():
324
            try:
325
                prev_token = request.user.auth_token
326
                user = form.save(request=request)
327
                next = restrict_next(
328
                    request.POST.get('next'),
329
                    domain=settings.COOKIE_DOMAIN
330
                )
331
                msg = _(astakos_messages.PROFILE_UPDATED)
332
                messages.success(request, msg)
333

    
334
                if form.email_changed:
335
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
336
                    messages.success(request, msg)
337
                if form.password_changed:
338
                    msg = _(astakos_messages.PASSWORD_CHANGED)
339
                    messages.success(request, msg)
340

    
341
                if next:
342
                    return redirect(next)
343
                else:
344
                    return redirect(reverse('edit_profile'))
345
            except ValueError, ve:
346
                messages.success(request, ve)
347
    elif request.method == "GET":
348
        request.user.is_verified = True
349
        request.user.save()
350

    
351
    # existing providers
352
    user_providers = request.user.get_enabled_auth_providers()
353
    user_disabled_providers = request.user.get_disabled_auth_providers()
354

    
355
    # providers that user can add
356
    user_available_providers = request.user.get_available_auth_providers()
357

    
358
    extra_context['services'] = Component.catalog().values()
359
    return render_response(template_name,
360
                           profile_form=form,
361
                           user_providers=user_providers,
362
                           user_disabled_providers=user_disabled_providers,
363
                           user_available_providers=user_available_providers,
364
                           context_instance=get_context(request,
365
                                                          extra_context))
366

    
367

    
368
@transaction.commit_manually
369
@require_http_methods(["GET", "POST"])
370
@cookie_fix
371
def signup(request, template_name='im/signup.html', on_success='index',
372
           extra_context=None, activation_backend=None):
373
    """
374
    Allows a user to create a local account.
375

376
    In case of GET request renders a form for entering the user information.
377
    In case of POST handles the signup.
378

379
    The user activation will be delegated to the backend specified by the
380
    ``activation_backend`` keyword argument if present, otherwise to the
381
    ``astakos.im.activation_backends.InvitationBackend`` if
382
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
383
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
384
    activation_backends);
385

386
    Upon successful user creation, if ``next`` url parameter is present the
387
    user is redirected there otherwise renders the same page with a success
388
    message.
389

390
    On unsuccessful creation, renders ``template_name`` with an error message.
391

392
    **Arguments**
393

394
    ``template_name``
395
        A custom template to render. This is optional;
396
        if not specified, this will default to ``im/signup.html``.
397

398
    ``extra_context``
399
        An dictionary of variables to add to the template context.
400

401
    ``on_success``
402
        Resolvable view name to redirect on registration success.
403

404
    **Template:**
405

406
    im/signup.html or ``template_name`` keyword argument.
407
    """
408
    extra_context = extra_context or {}
409
    if request.user.is_authenticated():
410
        logger.info("%s already signed in, redirect to index",
411
                    request.user.log_display)
412
        transaction.rollback()
413
        return HttpResponseRedirect(reverse('index'))
414

    
415
    provider = get_query(request).get('provider', 'local')
416
    if not auth.get_provider(provider).get_create_policy:
417
        logger.error("%s provider not available for signup", provider)
418
        transaction.rollback()
419
        raise PermissionDenied
420

    
421
    instance = None
422

    
423
    # user registered using third party provider
424
    third_party_token = request.REQUEST.get('third_party_token', None)
425
    unverified = None
426
    if third_party_token:
427
        # retreive third party entry. This was created right after the initial
428
        # third party provider handshake.
429
        pending = get_object_or_404(PendingThirdPartyUser,
430
                                    token=third_party_token)
431

    
432
        provider = pending.provider
433

    
434
        # clone third party instance into the corresponding AstakosUser
435
        instance = pending.get_user_instance()
436
        get_unverified = AstakosUserAuthProvider.objects.unverified
437

    
438
        # check existing unverified entries
439
        unverified = get_unverified(pending.provider,
440
                                    identifier=pending.third_party_identifier)
441

    
442
        if unverified and request.method == 'GET':
443
            messages.warning(request, unverified.get_pending_registration_msg)
444
            if unverified.user.moderated:
445
                messages.warning(request,
446
                                 unverified.get_pending_resend_activation_msg)
447
            else:
448
                messages.warning(request,
449
                                 unverified.get_pending_moderation_msg)
450

    
451
    # prepare activation backend based on current request
452
    if not activation_backend:
453
        activation_backend = activation_backends.get_backend()
454

    
455
    form_kwargs = {'instance': instance, 'request': request}
456
    if third_party_token:
457
        form_kwargs['third_party_token'] = third_party_token
458

    
459
    form = activation_backend.get_signup_form(
460
        provider, None, **form_kwargs)
461

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

    
468
        if form.is_valid():
469
            commited = False
470
            try:
471
                user = form.save(commit=False)
472

    
473
                # delete previously unverified accounts
474
                if AstakosUser.objects.user_exists(user.email):
475
                    AstakosUser.objects.get_by_identifier(user.email).delete()
476

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

    
489
                # commit user entry
490
                transaction.commit()
491
                # commited flag
492
                # in case an exception get raised from this point
493
                commited = True
494

    
495
                if user and user.is_active:
496
                    # activation backend directly activated the user
497
                    # log him in
498
                    next = request.POST.get('next', '')
499
                    response = prepare_response(request, user, next=next)
500
                    return response
501

    
502
                messages.add_message(request, status, message)
503
                return HttpResponseRedirect(reverse(on_success))
504
            except Exception, e:
505
                if not commited:
506
                    transaction.rollback()
507
                raise
508
    else:
509
        transaction.commit()
510

    
511
    return render_response(template_name,
512
                           signup_form=form,
513
                           third_party_token=third_party_token,
514
                           provider=provider,
515
                           context_instance=get_context(request, extra_context))
516

    
517

    
518
@require_http_methods(["GET", "POST"])
519
@required_auth_methods_assigned(allow_access=True)
520
@login_required
521
@cookie_fix
522
@signed_terms_required
523
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
524
    """
525
    Allows a user to send feedback.
526

527
    In case of GET request renders a form for providing the feedback information.
528
    In case of POST sends an email to support team.
529

530
    If the user isn't logged in, redirects to settings.LOGIN_URL.
531

532
    **Arguments**
533

534
    ``template_name``
535
        A custom template to use. This is optional; if not specified,
536
        this will default to ``im/feedback.html``.
537

538
    ``extra_context``
539
        An dictionary of variables to add to the template context.
540

541
    **Template:**
542

543
    im/signup.html or ``template_name`` keyword argument.
544

545
    **Settings:**
546

547
    * LOGIN_URL: login uri
548
    """
549
    extra_context = extra_context or {}
550
    if request.method == 'GET':
551
        form = FeedbackForm()
552
    if request.method == 'POST':
553
        if not request.user:
554
            return HttpResponse('Unauthorized', status=401)
555

    
556
        form = FeedbackForm(request.POST)
557
        if form.is_valid():
558
            msg = form.cleaned_data['feedback_msg']
559
            data = form.cleaned_data['feedback_data']
560
            send_feedback(msg, data, request.user, email_template_name)
561
            message = _(astakos_messages.FEEDBACK_SENT)
562
            messages.success(request, message)
563
            return HttpResponseRedirect(reverse('feedback'))
564

    
565
    return render_response(template_name,
566
                           feedback_form=form,
567
                           context_instance=get_context(request,
568
                                                        extra_context))
569

    
570

    
571
@require_http_methods(["GET"])
572
@cookie_fix
573
def logout(request, template='registration/logged_out.html',
574
           extra_context=None):
575
    """
576
    Wraps `django.contrib.auth.logout`.
577
    """
578
    extra_context = extra_context or {}
579
    response = HttpResponse()
580
    if request.user.is_authenticated():
581
        email = request.user.email
582
        auth_logout(request)
583
    else:
584
        response['Location'] = reverse('index')
585
        response.status_code = 301
586
        return response
587

    
588
    next = restrict_next(
589
        request.GET.get('next'),
590
        domain=settings.COOKIE_DOMAIN
591
    )
592

    
593
    if next:
594
        response['Location'] = next
595
        response.status_code = 302
596
    elif settings.LOGOUT_NEXT:
597
        response['Location'] = settings.LOGOUT_NEXT
598
        response.status_code = 301
599
    else:
600
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
601
        provider = auth.get_provider(last_provider)
602
        message = provider.get_logout_success_msg
603
        extra = provider.get_logout_success_extra_msg
604
        if extra:
605
            message += "<br />"  + extra
606
        messages.success(request, message)
607
        response['Location'] = reverse('index')
608
        response.status_code = 301
609
    return response
610

    
611

    
612
@require_http_methods(["GET", "POST"])
613
@cookie_fix
614
@transaction.commit_manually
615
def activate(request, greeting_email_template_name='im/welcome_email.txt',
616
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
617
    """
618
    Activates the user identified by the ``auth`` request parameter, sends a
619
    welcome email and renews the user token.
620

621
    The view uses commit_manually decorator in order to ensure the user state
622
    will be updated only if the email will be send successfully.
623
    """
624
    token = request.GET.get('auth')
625
    next = request.GET.get('next')
626

    
627
    if request.user.is_authenticated():
628
        message = _(astakos_messages.LOGGED_IN_WARNING)
629
        messages.error(request, message)
630
        transaction.rollback()
631
        return HttpResponseRedirect(reverse('index'))
632

    
633
    try:
634
        user = AstakosUser.objects.get(verification_code=token)
635
    except AstakosUser.DoesNotExist:
636
        transaction.rollback()
637
        raise Http404
638

    
639
    if user.email_verified:
640
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
641
        messages.error(request, message)
642
        return HttpResponseRedirect(reverse('index'))
643

    
644
    try:
645
        backend = activation_backends.get_backend()
646
        result = backend.handle_verification(user, token)
647
        backend.send_result_notifications(result, user)
648
        next = settings.ACTIVATION_REDIRECT_URL or next
649
        response = HttpResponseRedirect(reverse('index'))
650
        if user.is_active:
651
            response = prepare_response(request, user, next, renew=True)
652
            messages.success(request, _(result.message))
653
        else:
654
            messages.warning(request, _(result.message))
655
    except Exception:
656
        transaction.rollback()
657
        raise
658
    else:
659
        transaction.commit()
660
        return response
661

    
662

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

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

    
692
    terms = f.read()
693

    
694
    if request.method == 'POST':
695
        next = restrict_next(
696
            request.POST.get('next'),
697
            domain=settings.COOKIE_DOMAIN
698
        )
699
        if not next:
700
            next = reverse('index')
701
        form = SignApprovalTermsForm(request.POST, instance=request.user)
702
        if not form.is_valid():
703
            return render_response(template_name,
704
                                   terms=terms,
705
                                   approval_terms_form=form,
706
                                   context_instance=get_context(request,
707
                                                                extra_context))
708
        user = form.save()
709
        return HttpResponseRedirect(next)
710
    else:
711
        form = None
712
        if request.user.is_authenticated() and not request.user.signed_terms:
713
            form = SignApprovalTermsForm(instance=request.user)
714
        return render_response(template_name,
715
                               terms=terms,
716
                               approval_terms_form=form,
717
                               context_instance=get_context(request,
718
                                                            extra_context))
719

    
720

    
721
@require_http_methods(["GET", "POST"])
722
@cookie_fix
723
@transaction.commit_manually
724
def change_email(request, activation_key=None,
725
                 email_template_name='registration/email_change_email.txt',
726
                 form_template_name='registration/email_change_form.html',
727
                 confirm_template_name='registration/email_change_done.html',
728
                 extra_context=None):
729
    extra_context = extra_context or {}
730

    
731
    if not settings.EMAILCHANGE_ENABLED:
732
        raise PermissionDenied
733

    
734
    if activation_key:
735
        try:
736
            try:
737
                email_change = EmailChange.objects.get(
738
                    activation_key=activation_key)
739
            except EmailChange.DoesNotExist:
740
                transaction.rollback()
741
                logger.error("[change-email] Invalid or used activation "
742
                             "code, %s", activation_key)
743
                raise Http404
744

    
745
            if (request.user.is_authenticated() and \
746
                request.user == email_change.user) or not \
747
                    request.user.is_authenticated():
748
                user = EmailChange.objects.change_email(activation_key)
749
                msg = _(astakos_messages.EMAIL_CHANGED)
750
                messages.success(request, msg)
751
                transaction.commit()
752
                return HttpResponseRedirect(reverse('edit_profile'))
753
            else:
754
                logger.error("[change-email] Access from invalid user, %s %s",
755
                             email_change.user, request.user.log_display)
756
                transaction.rollback()
757
                raise PermissionDenied
758
        except ValueError, e:
759
            messages.error(request, e)
760
            transaction.rollback()
761
            return HttpResponseRedirect(reverse('index'))
762

    
763
        return render_response(confirm_template_name,
764
                               modified_user=user if 'user' in locals()
765
                               else None, context_instance=get_context(request,
766
                               extra_context))
767

    
768
    if not request.user.is_authenticated():
769
        path = quote(request.get_full_path())
770
        url = request.build_absolute_uri(reverse('index'))
771
        return HttpResponseRedirect(url + '?next=' + path)
772

    
773
    # clean up expired email changes
774
    if request.user.email_change_is_pending():
775
        change = request.user.emailchanges.get()
776
        if change.activation_key_expired():
777
            change.delete()
778
            transaction.commit()
779
            return HttpResponseRedirect(reverse('email_change'))
780

    
781
    form = EmailChangeForm(request.POST or None)
782
    if request.method == 'POST' and form.is_valid():
783
        try:
784
            ec = form.save(request, email_template_name, request)
785
        except Exception, e:
786
            transaction.rollback()
787
            raise
788
        else:
789
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
790
            messages.success(request, msg)
791
            transaction.commit()
792
            return HttpResponseRedirect(reverse('edit_profile'))
793

    
794
    if request.user.email_change_is_pending():
795
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
796

    
797
    return render_response(
798
        form_template_name,
799
        form=form,
800
        context_instance=get_context(request, extra_context)
801
    )
802

    
803

    
804
@cookie_fix
805
def send_activation(request, user_id, template_name='im/login.html',
806
                    extra_context=None):
807

    
808
    if request.user.is_authenticated():
809
        return HttpResponseRedirect(reverse('index'))
810

    
811
    extra_context = extra_context or {}
812
    try:
813
        u = AstakosUser.objects.get(id=user_id)
814
    except AstakosUser.DoesNotExist:
815
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
816
    else:
817
        if u.email_verified:
818
            logger.warning("[resend activation] Account already verified: %s",
819
                           u.log_display)
820

    
821
            messages.error(request,
822
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
823
        else:
824
            activation_backend = activation_backends.get_backend()
825
            activation_backend.send_user_verification_email(u)
826
            messages.success(request, astakos_messages.ACTIVATION_SENT)
827

    
828
    return HttpResponseRedirect(reverse('index'))
829

    
830

    
831
@require_http_methods(["GET"])
832
@cookie_fix
833
@valid_astakos_user_required
834
def resource_usage(request):
835

    
836
    resources_meta = presentation.RESOURCES
837

    
838
    current_usage = quotas.get_user_quotas(request.user)
839
    current_usage = json.dumps(current_usage['system'])
840
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
841
    if resource_catalog is False:
842
        # on fail resource_groups contains the result object
843
        result = resource_groups
844
        messages.error(request, 'Unable to retrieve system resources: %s' %
845
                       result.reason)
846

    
847
    resource_catalog = json.dumps(resource_catalog)
848
    resource_groups = json.dumps(resource_groups)
849
    resources_order = json.dumps(resources_meta.get('resources_order'))
850

    
851
    return render_response('im/resource_usage.html',
852
                           context_instance=get_context(request),
853
                           resource_catalog=resource_catalog,
854
                           resource_groups=resource_groups,
855
                           resources_order=resources_order,
856
                           current_usage=current_usage,
857
                           token_cookie_name=settings.COOKIE_NAME,
858
                           usage_update_interval=
859
                           settings.USAGE_UPDATE_INTERVAL)
860

    
861

    
862
# TODO: action only on POST and user should confirm the removal
863
@require_http_methods(["POST"])
864
@cookie_fix
865
@valid_astakos_user_required
866
def remove_auth_provider(request, pk):
867
    try:
868
        provider = request.user.auth_providers.get(pk=int(pk)).settings
869
    except AstakosUserAuthProvider.DoesNotExist:
870
        raise Http404
871

    
872
    if provider.get_remove_policy:
873
        messages.success(request, provider.get_removed_msg)
874
        provider.remove_from_user()
875
        return HttpResponseRedirect(reverse('edit_profile'))
876
    else:
877
        raise PermissionDenied
878

    
879

    
880
@require_http_methods(["GET"])
881
@required_auth_methods_assigned(allow_access=True)
882
@login_required
883
@cookie_fix
884
@signed_terms_required
885
def landing(request):
886
    context = {'services': Component.catalog(orderfor='dashboard')}
887
    return render_response(
888
        'im/landing.html',
889
        context_instance=get_context(request), **context)
890

    
891

    
892
@cookie_fix
893
def get_menu(request, with_extra_links=False, with_signout=True):
894
    user = request.user
895
    index_url = reverse('index')
896

    
897
    if isinstance(user, User) and user.is_authenticated():
898
        l = []
899
        append = l.append
900
        item = MenuItem
901
        item.current_path = request.build_absolute_uri(request.path)
902
        append(item(url=request.build_absolute_uri(reverse('index')),
903
                    name=user.email))
904
        if with_extra_links:
905
            append(item(url=request.build_absolute_uri(reverse('landing')),
906
                        name="Overview"))
907
        if with_signout:
908
            append(item(url=request.build_absolute_uri(reverse('landing')),
909
                        name="Dashboard"))
910
        if with_extra_links:
911
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
912
                        name="Profile"))
913

    
914
        if with_extra_links:
915
            if settings.INVITATIONS_ENABLED:
916
                append(item(url=request.build_absolute_uri(reverse('invite')),
917
                            name="Invitations"))
918

    
919
            append(item(url=request.build_absolute_uri(reverse('api_access')),
920
                        name="API access"))
921

    
922
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
923
                        name="Usage"))
924

    
925
            if settings.PROJECTS_VISIBLE:
926
                append(item(url=request.build_absolute_uri(reverse('project_list')),
927
                            name="Projects"))
928

    
929
            append(item(url=request.build_absolute_uri(reverse('feedback')),
930
                        name="Contact"))
931
        if with_signout:
932
            append(item(url=request.build_absolute_uri(reverse('logout')),
933
                        name="Sign out"))
934
    else:
935
        l = [{'url': request.build_absolute_uri(index_url),
936
              'name': _("Sign in")}]
937

    
938
    callback = request.GET.get('callback', None)
939
    data = json.dumps(tuple(l))
940
    mimetype = 'application/json'
941

    
942
    if callback:
943
        mimetype = 'application/javascript'
944
        data = '%s(%s)' % (callback, data)
945

    
946
    return HttpResponse(content=data, mimetype=mimetype)
947

    
948

    
949
class MenuItem(dict):
950
    current_path = ''
951

    
952
    def __init__(self, *args, **kwargs):
953
        super(MenuItem, self).__init__(*args, **kwargs)
954
        if kwargs.get('url') or kwargs.get('submenu'):
955
            self.__set_is_active__()
956

    
957
    def __setitem__(self, key, value):
958
        super(MenuItem, self).__setitem__(key, value)
959
        if key in ('url', 'submenu'):
960
            self.__set_is_active__()
961

    
962
    def __set_is_active__(self):
963
        if self.get('is_active'):
964
            return
965
        if self.current_path.startswith(self.get('url')):
966
            self.__setitem__('is_active', True)
967
        else:
968
            submenu = self.get('submenu', ())
969
            current = (i for i in submenu if i.get('url') == self.current_path)
970
            try:
971
                current_node = current.next()
972
                if not current_node.get('is_active'):
973
                    current_node.__setitem__('is_active', True)
974
                self.__setitem__('is_active', True)
975
            except StopIteration:
976
                return
977

    
978
    def __setattribute__(self, name, value):
979
        super(MenuItem, self).__setattribute__(name, value)
980
        if name == 'current_path':
981
            self.__set_is_active__()
982

    
983

    
984
def get_services(request):
985
    callback = request.GET.get('callback', None)
986
    mimetype = 'application/json'
987
    data = json.dumps(Component.catalog().values())
988

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

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