Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (35.1 kB)

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

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.contrib.auth.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
from django.conf import settings as django_settings
81

    
82
logger = logging.getLogger(__name__)
83

    
84

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

92
    **Arguments**
93

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

98
    ``extra_context``
99
        An dictionary of variables to add to the template context.
100
    """
101

    
102
    extra_context = extra_context or {}
103

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

    
108
    if request.user.is_authenticated():
109
        return HttpResponseRedirect(reverse('landing'))
110

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

    
117

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

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

    
132

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

    
146

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

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

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

162
    If the user isn't logged in, redirects to settings.LOGIN_URL.
163

164
    **Arguments**
165

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

170
    ``extra_context``
171
        An dictionary of variables to add to the template context.
172

173
    **Template:**
174

175
    im/invitations.html or ``template_name`` keyword argument.
176

177
    **Settings:**
178

179
    The view expectes the following settings are defined:
180

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

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

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

    
219

    
220

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

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

    
235
    url = get_public_endpoint(django_settings.SYNNEFO_SERVICES, 'identity')
236

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

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

    
255

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

    
267
    url = get_public_endpoint(django_settings.SYNNEFO_SERVICES, 'identity')
268
    context['services'] = Component.catalog()
269
    context['token_url'] = url
270
    context['client_url'] = settings.API_CLIENT_URL
271

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

    
278

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

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

292
    If the user isn't logged in, redirects to settings.LOGIN_URL.
293

294
    **Arguments**
295

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

300
    ``extra_context``
301
        An dictionary of variables to add to the template context.
302

303
    **Template:**
304

305
    im/profile.html or ``template_name`` keyword argument.
306

307
    **Settings:**
308

309
    The view expectes the following settings are defined:
310

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

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

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

    
353
    # existing providers
354
    user_providers = request.user.get_enabled_auth_providers()
355
    user_disabled_providers = request.user.get_disabled_auth_providers()
356

    
357
    # providers that user can add
358
    user_available_providers = request.user.get_available_auth_providers()
359

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

    
369

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

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

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

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

392
    On unsuccessful creation, renders ``template_name`` with an error message.
393

394
    **Arguments**
395

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

400
    ``extra_context``
401
        An dictionary of variables to add to the template context.
402

403
    ``on_success``
404
        Resolvable view name to redirect on registration success.
405

406
    **Template:**
407

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

    
416
    provider = get_query(request).get('provider', 'local')
417
    if not auth.get_provider(provider).get_create_policy:
418
        logger.error("%s provider not available for signup", provider)
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}
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

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

    
515

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

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

528
    If the user isn't logged in, redirects to settings.LOGIN_URL.
529

530
    **Arguments**
531

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

536
    ``extra_context``
537
        An dictionary of variables to add to the template context.
538

539
    **Template:**
540

541
    im/signup.html or ``template_name`` keyword argument.
542

543
    **Settings:**
544

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

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

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

    
568

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

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

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

    
609

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

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

    
625
    if request.user.is_authenticated():
626
        message = _(astakos_messages.LOGGED_IN_WARNING)
627
        messages.error(request, message)
628
        return HttpResponseRedirect(reverse('index'))
629

    
630
    try:
631
        user = AstakosUser.objects.get(verification_code=token)
632
    except AstakosUser.DoesNotExist:
633
        raise Http404
634

    
635
    if user.email_verified:
636
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
637
        messages.error(request, message)
638
        return HttpResponseRedirect(reverse('index'))
639

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

    
658

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

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

    
688
    terms = f.read()
689

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

    
716

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

    
727
    if not settings.EMAILCHANGE_ENABLED:
728
        raise PermissionDenied
729

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

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

    
759
        return render_response(confirm_template_name,
760
                               modified_user=user if 'user' in locals()
761
                               else None, context_instance=get_context(request,
762
                               extra_context))
763

    
764
    if not request.user.is_authenticated():
765
        path = quote(request.get_full_path())
766
        url = request.build_absolute_uri(reverse('index'))
767
        return HttpResponseRedirect(url + '?next=' + path)
768

    
769
    # clean up expired email changes
770
    if request.user.email_change_is_pending():
771
        change = request.user.emailchanges.get()
772
        if change.activation_key_expired():
773
            change.delete()
774
            transaction.commit()
775
            return HttpResponseRedirect(reverse('email_change'))
776

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

    
790
    if request.user.email_change_is_pending():
791
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
792

    
793
    return render_response(
794
        form_template_name,
795
        form=form,
796
        context_instance=get_context(request, extra_context)
797
    )
798

    
799

    
800
@cookie_fix
801
def send_activation(request, user_id, template_name='im/login.html',
802
                    extra_context=None):
803

    
804
    if request.user.is_authenticated():
805
        return HttpResponseRedirect(reverse('index'))
806

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

    
817
            messages.error(request,
818
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
819
        else:
820
            activation_backend = activation_backends.get_backend()
821
            activation_backend.send_user_verification_email(u)
822
            messages.success(request, astakos_messages.ACTIVATION_SENT)
823

    
824
    return HttpResponseRedirect(reverse('index'))
825

    
826

    
827
@require_http_methods(["GET"])
828
@cookie_fix
829
@valid_astakos_user_required
830
def resource_usage(request):
831

    
832
    resources_meta = presentation.RESOURCES
833

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

    
843
    resource_catalog = json.dumps(resource_catalog)
844
    resource_groups = json.dumps(resource_groups)
845
    resources_order = json.dumps(resources_meta.get('resources_order'))
846

    
847
    return render_response('im/resource_usage.html',
848
                           context_instance=get_context(request),
849
                           resource_catalog=resource_catalog,
850
                           resource_groups=resource_groups,
851
                           resources_order=resources_order,
852
                           current_usage=current_usage,
853
                           token_cookie_name=settings.COOKIE_NAME,
854
                           usage_update_interval=
855
                           settings.USAGE_UPDATE_INTERVAL)
856

    
857

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

    
868
    if provider.get_remove_policy:
869
        messages.success(request, provider.get_removed_msg)
870
        provider.remove_from_user()
871
        return HttpResponseRedirect(reverse('edit_profile'))
872
    else:
873
        raise PermissionDenied
874

    
875

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

    
887

    
888
@cookie_fix
889
def get_menu(request, with_extra_links=False, with_signout=True):
890
    user = request.user
891
    index_url = reverse('index')
892

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

    
910
        if with_extra_links:
911
            if settings.INVITATIONS_ENABLED:
912
                append(item(url=request.build_absolute_uri(reverse('invite')),
913
                            name="Invitations"))
914

    
915
            append(item(url=request.build_absolute_uri(reverse('api_access')),
916
                        name="API access"))
917

    
918
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
919
                        name="Usage"))
920

    
921
            if settings.PROJECTS_VISIBLE:
922
                append(item(url=request.build_absolute_uri(reverse('project_list')),
923
                            name="Projects"))
924

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

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

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

    
942
    return HttpResponse(content=data, mimetype=mimetype)
943

    
944

    
945
class MenuItem(dict):
946
    current_path = ''
947

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

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

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

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

    
979

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

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

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