Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 3466f0a3

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
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()
247
    response.status_code = 200
248
    response.content_type = content_type
249
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
250
    response.content = content
251
    return response
252

    
253

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

    
265
    url = get_public_endpoint(settings.astakos_services, 'identity')
266
    context['services'] = Component.catalog()
267
    context['token_url'] = url
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
        return HttpResponseRedirect(reverse('index'))
413

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

    
419
    instance = None
420

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

    
430
        provider = pending.provider
431

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

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

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

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

    
453
    form_kwargs = {'instance': instance}
454
    if third_party_token:
455
        form_kwargs['third_party_token'] = third_party_token
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
            commited = False
468
            try:
469
                user = form.save(commit=False)
470

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

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

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

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

    
500
                messages.add_message(request, status, message)
501
                return HttpResponseRedirect(reverse(on_success))
502
            except Exception, e:
503
                if not commited:
504
                    transaction.rollback()
505
                raise
506

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

    
513

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

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

526
    If the user isn't logged in, redirects to settings.LOGIN_URL.
527

528
    **Arguments**
529

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

534
    ``extra_context``
535
        An dictionary of variables to add to the template context.
536

537
    **Template:**
538

539
    im/signup.html or ``template_name`` keyword argument.
540

541
    **Settings:**
542

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

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

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

    
566

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

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

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

    
607

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

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

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

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

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

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

    
656

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

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

    
686
    terms = f.read()
687

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

    
714

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

    
725
    if not settings.EMAILCHANGE_ENABLED:
726
        raise PermissionDenied
727

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

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

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

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

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

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

    
788
    if request.user.email_change_is_pending():
789
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
790

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

    
797

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

    
802
    if request.user.is_authenticated():
803
        return HttpResponseRedirect(reverse('index'))
804

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

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

    
822
    return HttpResponseRedirect(reverse('index'))
823

    
824

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

    
830
    resources_meta = presentation.RESOURCES
831

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

    
841
    resource_catalog = json.dumps(resource_catalog)
842
    resource_groups = json.dumps(resource_groups)
843
    resources_order = json.dumps(resources_meta.get('resources_order'))
844

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

    
855

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

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

    
873

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

    
885

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

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

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

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

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

    
919
            if settings.PROJECTS_VISIBLE:
920
                append(item(url=request.build_absolute_uri(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)