Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (35.5 kB)

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

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

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

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

    
57
import astakos.im.messages as astakos_messages
58

    
59
from astakos.im import activation_backends
60
from astakos.im.models import AstakosUser, ApprovalTerms, EmailChange, \
61
    AstakosUserAuthProvider, PendingThirdPartyUser, Component
62
from astakos.im.util import get_context, prepare_response, get_query, \
63
    restrict_next
64
from astakos.im.forms import LoginForm, InvitationForm, FeedbackForm, \
65
    SignApprovalTermsForm, EmailChangeForm
66
from astakos.im.forms import ExtendedProfileForm as ProfileForm
67
from synnefo.lib.services import get_public_endpoint
68
from astakos.im.functions import send_feedback, logout as auth_logout, \
69
    invite as invite_func
70
from astakos.im import settings
71
from astakos.im import presentation
72
from astakos.im import auth_providers as auth
73
from astakos.im import quotas
74
from astakos.im.views.util import render_response, _resources_catalog
75
from astakos.im.views.decorators import cookie_fix, signed_terms_required,\
76
    required_auth_methods_assigned, valid_astakos_user_required, login_required
77
from astakos.api import projects as projects_api
78
from astakos.api.util import _dthandler
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
@transaction.commit_on_success
135
def update_token(request):
136
    """
137
    Update api token view.
138
    """
139
    user = AstakosUser.objects.select_for_update().get(id=request.user.id)
140
    user.renew_token()
141
    user.save()
142
    messages.success(request, astakos_messages.TOKEN_UPDATED)
143
    return HttpResponseRedirect(reverse('api_access'))
144

    
145

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

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

159
    The number of the user invitations is going to be updated only if the email
160
    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
                email = form.cleaned_data.get('username')
194
                realname = form.cleaned_data.get('realname')
195
                invite_func(inviter, email, realname)
196
                message = _(astakos_messages.INVITATION_SENT) % locals()
197
                messages.success(request, message)
198
        else:
199
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
200
            messages.error(request, message)
201

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

    
213

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

    
223
    if settings.KAMAKI_CONFIG_CLOUD_NAME:
224
        cloud_name = settings.KAMAKI_CONFIG_CLOUD_NAME
225
    else:
226
        cloud_name = branding_settings.SERVICE_NAME.replace(' ', '_').lower()
227

    
228
    url = get_public_endpoint(settings.astakos_services, 'identity')
229

    
230
    context = {
231
        'user': request.user,
232
        'services': Component.catalog(),
233
        'token_url': url,
234
        'cloud_name': cloud_name
235
    }
236

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

    
247

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

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

    
265
    if extra_context:
266
        context.update(extra_context)
267
    context_instance = get_context(request, context)
268
    return render_response(template_name,
269
                           context_instance=context_instance)
270

    
271

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

282
    In case of GET request renders a form for displaying the user information.
283
    In case of POST updates the user informantion and redirects to ``next``
284
    url parameter if exists.
285

286
    If the user isn't logged in, redirects to settings.LOGIN_URL.
287

288
    **Arguments**
289

290
    ``template_name``
291
        A custom template to use. This is optional; if not specified,
292
        this will default to ``im/profile.html``.
293

294
    ``extra_context``
295
        An dictionary of variables to add to the template context.
296

297
    **Template:**
298

299
    im/profile.html or ``template_name`` keyword argument.
300

301
    **Settings:**
302

303
    The view expectes the following settings are defined:
304

305
    * LOGIN_URL: login uri
306
    """
307

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

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

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

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

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

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

    
366

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

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

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

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

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

391
    **Arguments**
392

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

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

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

403
    **Template:**
404

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

    
413
    provider = get_query(request).get('provider', 'local')
414
    try:
415
        auth.get_provider(provider)
416
    except auth.InvalidProvider, e:
417
        messages.error(request, e.message)
418
        return HttpResponseRedirect(reverse("signup"))
419

    
420
    if not auth.get_provider(provider).get_create_policy:
421
        logger.error("%s provider not available for signup", provider)
422
        raise PermissionDenied
423

    
424
    instance = None
425

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

    
436
        provider = pending.provider
437

    
438
        # clone third party instance into the corresponding AstakosUser
439
        instance = pending.get_user_instance()
440
        get_unverified = AstakosUserAuthProvider.objects.unverified
441

    
442
        # check existing unverified entries
443
        unverified = get_unverified(pending.provider,
444
                                    identifier=pending.third_party_identifier)
445

    
446
        get_verified = AstakosUserAuthProvider.objects.verified
447
        verified = get_verified(pending.provider,
448
                                identifier=pending.third_party_identifier)
449
        if verified:
450
            # an existing verified user already exists for the third party
451
            # identifier
452
            pending.delete()
453
            raise Http404
454

    
455
        if unverified and request.method == 'GET':
456
            messages.warning(request, unverified.get_pending_registration_msg)
457

    
458
    # prepare activation backend based on current request
459
    if not activation_backend:
460
        activation_backend = activation_backends.get_backend()
461

    
462
    form_kwargs = {'instance': instance, 'request': request}
463
    if third_party_token:
464
        form_kwargs['third_party_token'] = third_party_token
465

    
466
    if pending:
467
        form_kwargs['initial'] = {
468
            'first_name': pending.first_name,
469
            'last_name': pending.last_name,
470
            'email': pending.email
471
        }
472

    
473
    form = activation_backend.get_signup_form(
474
        provider, None, **form_kwargs)
475

    
476

    
477
    if request.method == 'POST':
478
        form = activation_backend.get_signup_form(
479
            provider,
480
            request.POST,
481
            **form_kwargs)
482

    
483
        if form.is_valid():
484
            user = form.create_user()
485
            result = activation_backend.handle_registration(user)
486
            if result.status == \
487
                    activation_backend.Result.PENDING_MODERATION:
488
                # user should be warned that his account is not active yet
489
                status = messages.WARNING
490
            else:
491
                status = messages.SUCCESS
492
            message = result.message
493
            activation_backend.send_result_notifications(result, user)
494

    
495
            # commit user entry
496
            transaction.commit()
497

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

    
505
            messages.add_message(request, status, message)
506
            return HttpResponseRedirect(reverse(on_success))
507

    
508
    return render_response(
509
        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',
522
             email_template_name='im/feedback_mail.txt', extra_context=None):
523
    """
524
    Allows a user to send feedback.
525

526
    In case of GET request renders a form for providing the feedback
527
    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(
601
            'astakos_last_login_method', 'local')
602
        try:
603
            provider = auth.get_provider(last_provider)
604
        except auth.InvalidProvider:
605
            provider = auth.get_provider('local')
606

    
607
        message = provider.get_logout_success_msg
608
        extra = provider.get_logout_success_extra_msg
609

    
610
        if extra:
611
            message += "<br />" + extra
612
        messages.success(request, message)
613
        response['Location'] = reverse('index')
614
        response.status_code = 301
615
    return response
616

    
617

    
618
@require_http_methods(["GET", "POST"])
619
@cookie_fix
620
@transaction.commit_on_success
621
def activate(request, greeting_email_template_name='im/welcome_email.txt',
622
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
623
    """
624
    Activates the user identified by the ``auth`` request parameter, sends a
625
    welcome email and renews the user token.
626

627
    The user state will be updated only if the email will be send successfully.
628
    """
629
    token = request.GET.get('auth', None)
630
    next = request.GET.get('next', None)
631

    
632
    if not token:
633
        raise PermissionDenied
634

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

    
640
    try:
641
        user = AstakosUser.objects.select_for_update().\
642
            get(verification_code=token)
643
    except AstakosUser.DoesNotExist:
644
        messages.error(request, astakos_messages.INVALID_ACTIVATION_KEY)
645
        return HttpResponseRedirect(reverse('index'))
646

    
647
    if user.email_verified:
648
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
649
        messages.error(request, message)
650
        return HttpResponseRedirect(reverse('index'))
651

    
652
    backend = activation_backends.get_backend()
653
    result = backend.handle_verification(user, token)
654
    backend.send_result_notifications(result, user)
655
    next = settings.ACTIVATION_REDIRECT_URL or next or reverse('index')
656
    if user.is_active:
657
        response = prepare_response(request, user, next, renew=True)
658
        messages.success(request, _(result.message))
659
    else:
660
        response = HttpResponseRedirect(reverse('index'))
661
        messages.warning(request, _(result.message))
662

    
663
    return response
664

    
665

    
666
@login_required
667
def _approval_terms_post(request, template_name, terms, extra_context):
668
    next = restrict_next(
669
        request.POST.get('next'),
670
        domain=settings.COOKIE_DOMAIN
671
    )
672
    if not next:
673
        next = reverse('index')
674
    form = SignApprovalTermsForm(request.POST, instance=request.user)
675
    if not form.is_valid():
676
        return render_response(template_name,
677
                               terms=terms,
678
                               approval_terms_form=form,
679
                               context_instance=get_context(request,
680
                                                            extra_context))
681
    user = form.save()
682
    return HttpResponseRedirect(next)
683

    
684

    
685
@require_http_methods(["GET", "POST"])
686
@cookie_fix
687
def approval_terms(request, term_id=None,
688
                   template_name='im/approval_terms.html', extra_context=None):
689
    extra_context = extra_context or {}
690
    terms_record = None
691
    terms = None
692
    if not term_id:
693
        try:
694
            terms_record = ApprovalTerms.objects.order_by('-id')[0]
695
        except IndexError:
696
            pass
697
    else:
698
        try:
699
            terms_record = ApprovalTerms.objects.get(id=term_id)
700
        except ApprovalTerms.DoesNotExist, e:
701
            pass
702

    
703
    if not terms_record:
704
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
705
        return HttpResponseRedirect(reverse('index'))
706
    try:
707
        f = open(terms_record.location, 'r')
708
    except IOError:
709
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
710
        return render_response(
711
            template_name, context_instance=get_context(request,
712
                                                        extra_context))
713

    
714
    terms = f.read()
715

    
716
    if request.method == 'POST':
717
        return _approval_terms_post(request, template_name, terms,
718
                                    extra_context)
719
    else:
720
        form = None
721
        if request.user.is_authenticated() and not request.user.signed_terms:
722
            form = SignApprovalTermsForm(instance=request.user)
723
        return render_response(template_name,
724
                               terms=terms,
725
                               approval_terms_form=form,
726
                               context_instance=get_context(request,
727
                                                            extra_context))
728

    
729

    
730
@require_http_methods(["GET", "POST"])
731
@cookie_fix
732
@transaction.commit_on_success
733
def change_email(request, activation_key=None,
734
                 email_template_name='registration/email_change_email.txt',
735
                 form_template_name='registration/email_change_form.html',
736
                 confirm_template_name='registration/email_change_done.html',
737
                 extra_context=None):
738
    extra_context = extra_context or {}
739

    
740
    if not settings.EMAILCHANGE_ENABLED:
741
        raise PermissionDenied
742

    
743
    if activation_key:
744
        try:
745
            try:
746
                email_change = EmailChange.objects.get(
747
                    activation_key=activation_key)
748
            except EmailChange.DoesNotExist:
749
                logger.error("[change-email] Invalid or used activation "
750
                             "code, %s", activation_key)
751
                raise Http404
752

    
753
            if (
754
                request.user.is_authenticated() and
755
                request.user == email_change.user or not
756
                request.user.is_authenticated()
757
            ):
758
                user = EmailChange.objects.change_email(activation_key)
759
                msg = _(astakos_messages.EMAIL_CHANGED)
760
                messages.success(request, msg)
761
                transaction.commit()
762
                return HttpResponseRedirect(reverse('edit_profile'))
763
            else:
764
                logger.error("[change-email] Access from invalid user, %s %s",
765
                             email_change.user, request.user.log_display)
766
                raise PermissionDenied
767
        except ValueError, e:
768
            messages.error(request, e)
769
            transaction.rollback()
770
            return HttpResponseRedirect(reverse('index'))
771

    
772
        return render_response(confirm_template_name,
773
                               modified_user=user if 'user' in locals()
774
                               else None,
775
                               context_instance=get_context(request,
776
                                                            extra_context))
777

    
778
    if not request.user.is_authenticated():
779
        path = quote(request.get_full_path())
780
        url = request.build_absolute_uri(reverse('index'))
781
        return HttpResponseRedirect(url + '?next=' + path)
782

    
783
    # clean up expired email changes
784
    if request.user.email_change_is_pending():
785
        change = request.user.emailchanges.get()
786
        if change.activation_key_expired():
787
            change.delete()
788
            transaction.commit()
789
            return HttpResponseRedirect(reverse('email_change'))
790

    
791
    form = EmailChangeForm(request.POST or None)
792
    if request.method == 'POST' and form.is_valid():
793
        ec = form.save(request, email_template_name, request)
794
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
795
        messages.success(request, msg)
796
        transaction.commit()
797
        return HttpResponseRedirect(reverse('edit_profile'))
798

    
799
    if request.user.email_change_is_pending():
800
        messages.warning(request,
801
                         astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
802

    
803
    return render_response(
804
        form_template_name,
805
        form=form,
806
        context_instance=get_context(request, extra_context)
807
    )
808

    
809

    
810
@cookie_fix
811
@transaction.commit_on_success
812
def send_activation(request, user_id, template_name='im/login.html',
813
                    extra_context=None):
814

    
815
    if request.user.is_authenticated():
816
        return HttpResponseRedirect(reverse('index'))
817

    
818
    extra_context = extra_context or {}
819
    try:
820
        u = AstakosUser.objects.select_for_update().get(id=user_id)
821
    except AstakosUser.DoesNotExist:
822
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
823
    else:
824
        if u.email_verified:
825
            logger.warning("[resend activation] Account already verified: %s",
826
                           u.log_display)
827

    
828
            messages.error(request,
829
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
830
        else:
831
            activation_backend = activation_backends.get_backend()
832
            activation_backend.send_user_verification_email(u)
833
            messages.success(request, astakos_messages.ACTIVATION_SENT)
834

    
835
    return HttpResponseRedirect(reverse('index'))
836

    
837

    
838
@require_http_methods(["GET"])
839
@cookie_fix
840
@valid_astakos_user_required
841
def resource_usage(request):
842

    
843
    resources_meta = presentation.RESOURCES
844

    
845
    user_memberships = request.user.projectmembership_set.actually_accepted()
846
    sources = [quotas.project_ref(m.project.uuid) for m in user_memberships]
847
    user_quotas = quotas.get_user_quotas(request.user, sources=sources)
848
    projects = [m.project for m in user_memberships]
849
    user_projects = projects_api.get_projects_details(projects)
850
    resource_catalog, resource_groups = _resources_catalog()
851
    if resource_catalog is False:
852
        # on fail resource_groups contains the result object
853
        result = resource_groups
854
        messages.error(request, 'Unable to retrieve system resources: %s' %
855
                       result.reason)
856

    
857
    resource_catalog = json.dumps(resource_catalog)
858
    resource_groups = json.dumps(resource_groups)
859
    resources_order = json.dumps(resources_meta.get('resources_order'))
860
    projects_details = json.dumps(user_projects, default=_dthandler)
861
    user_quotas = json.dumps(user_quotas)
862

    
863

    
864
    interval = settings.USAGE_UPDATE_INTERVAL
865
    return render_response('im/resource_usage.html',
866
                           context_instance=get_context(request),
867
                           resource_catalog=resource_catalog,
868
                           resource_groups=resource_groups,
869
                           resources_order=resources_order,
870
                           projects_details=projects_details,
871
                           user_quotas=user_quotas,
872
                           token_cookie_name=settings.COOKIE_NAME,
873
                           usage_update_interval=interval)
874

    
875

    
876
# TODO: action only on POST and user should confirm the removal
877
@require_http_methods(["POST"])
878
@cookie_fix
879
@valid_astakos_user_required
880
def remove_auth_provider(request, pk):
881
    try:
882
        provider = request.user.auth_providers.get(pk=int(pk)).settings
883
    except AstakosUserAuthProvider.DoesNotExist:
884
        raise Http404
885

    
886
    if provider.get_remove_policy:
887
        messages.success(request, provider.get_removed_msg)
888
        provider.remove_from_user()
889
        return HttpResponseRedirect(reverse('edit_profile'))
890
    else:
891
        raise PermissionDenied
892

    
893

    
894
@require_http_methods(["GET"])
895
@required_auth_methods_assigned(allow_access=True)
896
@login_required
897
@cookie_fix
898
@signed_terms_required
899
def landing(request):
900
    context = {'services': Component.catalog(orderfor='dashboard')}
901
    return render_response(
902
        'im/landing.html',
903
        context_instance=get_context(request), **context)
904

    
905

    
906
@cookie_fix
907
def get_menu(request, with_extra_links=False, with_signout=True):
908
    user = request.user
909
    index_url = reverse('index')
910

    
911
    if isinstance(user, User) and user.is_authenticated():
912
        l = []
913
        append = l.append
914
        item = MenuItem
915
        item.current_path = request.build_absolute_uri(request.path)
916
        append(item(url=request.build_absolute_uri(reverse('index')),
917
                    name=user.email))
918
        if with_extra_links:
919
            append(item(url=request.build_absolute_uri(reverse('landing')),
920
                        name="Overview"))
921
        if with_signout:
922
            append(item(url=request.build_absolute_uri(reverse('landing')),
923
                        name="Dashboard"))
924
        if with_extra_links:
925
            append(
926
                item(
927
                    url=request.build_absolute_uri(reverse('edit_profile')),
928
                    name="Profile"))
929

    
930
        if with_extra_links:
931
            if settings.INVITATIONS_ENABLED:
932
                append(item(url=request.build_absolute_uri(reverse('invite')),
933
                            name="Invitations"))
934

    
935
            append(item(url=request.build_absolute_uri(reverse('api_access')),
936
                        name="API access"))
937

    
938
            append(
939
                item(
940
                    url=request.build_absolute_uri(reverse('resource_usage')),
941
                    name="Usage"))
942

    
943
            append(
944
                item(
945
                    url=request.build_absolute_uri(
946
                        reverse('project_list')),
947
                    name="Projects"))
948

    
949
            append(item(url=request.build_absolute_uri(reverse('feedback')),
950
                        name="Contact"))
951
        if with_signout:
952
            append(item(url=request.build_absolute_uri(reverse('logout')),
953
                        name="Sign out"))
954
    else:
955
        l = [{'url': request.build_absolute_uri(index_url),
956
              'name': _("Sign in")}]
957

    
958
    callback = request.GET.get('callback', None)
959
    data = json.dumps(tuple(l))
960
    mimetype = 'application/json'
961

    
962
    if callback:
963
        mimetype = 'application/javascript'
964
        data = '%s(%s)' % (callback, data)
965

    
966
    return HttpResponse(content=data, mimetype=mimetype)
967

    
968

    
969
class MenuItem(dict):
970
    current_path = ''
971

    
972
    def __init__(self, *args, **kwargs):
973
        super(MenuItem, self).__init__(*args, **kwargs)
974
        if kwargs.get('url') or kwargs.get('submenu'):
975
            self.__set_is_active__()
976

    
977
    def __setitem__(self, key, value):
978
        super(MenuItem, self).__setitem__(key, value)
979
        if key in ('url', 'submenu'):
980
            self.__set_is_active__()
981

    
982
    def __set_is_active__(self):
983
        if self.get('is_active'):
984
            return
985
        if self.current_path.startswith(self.get('url')):
986
            self.__setitem__('is_active', True)
987
        else:
988
            submenu = self.get('submenu', ())
989
            current = (i for i in submenu if i.get('url') == self.current_path)
990
            try:
991
                current_node = current.next()
992
                if not current_node.get('is_active'):
993
                    current_node.__setitem__('is_active', True)
994
                self.__setitem__('is_active', True)
995
            except StopIteration:
996
                return
997

    
998
    def __setattribute__(self, name, value):
999
        super(MenuItem, self).__setattribute__(name, value)
1000
        if name == 'current_path':
1001
            self.__set_is_active__()
1002

    
1003

    
1004
def get_services(request):
1005
    callback = request.GET.get('callback', None)
1006
    mimetype = 'application/json'
1007
    data = json.dumps(Component.catalog().values())
1008

    
1009
    if callback:
1010
        # Consume session messages. When get_services is loaded from an astakos
1011
        # page, messages should have already been consumed in the html
1012
        # response. When get_services is loaded from another domain/service we
1013
        # consume them here so that no stale messages to appear if user visits
1014
        # an astakos view later on.
1015
        # TODO: messages could be served to other services/sites in the dict
1016
        # response of get_services and/or get_menu. Services could handle those
1017
        # messages respectively.
1018
        messages_list = list(messages.get_messages(request))
1019
        mimetype = 'application/javascript'
1020
        data = '%s(%s)' % (callback, data)
1021

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