Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (35.1 kB)

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

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

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

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

    
57
import astakos.im.messages as astakos_messages
58

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

    
78
logger = logging.getLogger(__name__)
79

    
80

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

88
    **Arguments**
89

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

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

    
98
    extra_context = extra_context or {}
99

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

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

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

    
113

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

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

    
128

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

    
143

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

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

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

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

    
211

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

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

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

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

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

    
245

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

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

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

    
269

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

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

284
    If the user isn't logged in, redirects to settings.LOGIN_URL.
285

286
    **Arguments**
287

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

292
    ``extra_context``
293
        An dictionary of variables to add to the template context.
294

295
    **Template:**
296

297
    im/profile.html or ``template_name`` keyword argument.
298

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

303
    * LOGIN_URL: login uri
304
    """
305

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

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

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

    
348
    # existing providers
349
    user_providers = request.user.get_enabled_auth_providers()
350
    user_disabled_providers = request.user.get_disabled_auth_providers()
351

    
352
    # providers that user can add
353
    user_available_providers = request.user.get_available_auth_providers()
354

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

    
364

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

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

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

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

387
    On unsuccessful creation, renders ``template_name`` with an error message.
388

389
    **Arguments**
390

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

395
    ``extra_context``
396
        An dictionary of variables to add to the template context.
397

398
    ``on_success``
399
        Resolvable view name to redirect on registration success.
400

401
    **Template:**
402

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

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

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

    
422
    instance = None
423

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

    
434
        provider = pending.provider
435

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

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

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

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

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

    
460
    form_kwargs = {'instance': instance, 'request': request}
461
    if third_party_token:
462
        form_kwargs['third_party_token'] = third_party_token
463

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

    
471
    form = activation_backend.get_signup_form(
472
        provider, None, **form_kwargs)
473

    
474

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

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

    
493
            # commit user entry
494
            transaction.commit()
495

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

    
503
            messages.add_message(request, status, message)
504
            return HttpResponseRedirect(reverse(on_success))
505

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

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

    
605
        message = provider.get_logout_success_msg
606
        extra = provider.get_logout_success_extra_msg
607

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

    
615

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

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

    
630
    if not token:
631
        raise PermissionDenied
632

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

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

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

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

    
661
    return response
662

    
663

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

    
682

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

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

    
712
    terms = f.read()
713

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

    
727

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

    
738
    if not settings.EMAILCHANGE_ENABLED:
739
        raise PermissionDenied
740

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

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

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

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

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

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

    
797
    if request.user.email_change_is_pending():
798
        messages.warning(request,
799
                         astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
800

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

    
807

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

    
813
    if request.user.is_authenticated():
814
        return HttpResponseRedirect(reverse('index'))
815

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

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

    
833
    return HttpResponseRedirect(reverse('index'))
834

    
835

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

    
841
    resources_meta = presentation.RESOURCES
842

    
843
    current_usage = quotas.get_user_quotas(request.user)
844
    current_usage = json.dumps(current_usage['system'])
845
    resource_catalog, resource_groups = _resources_catalog()
846
    if resource_catalog is False:
847
        # on fail resource_groups contains the result object
848
        result = resource_groups
849
        messages.error(request, 'Unable to retrieve system resources: %s' %
850
                       result.reason)
851

    
852
    resource_catalog = json.dumps(resource_catalog)
853
    resource_groups = json.dumps(resource_groups)
854
    resources_order = json.dumps(resources_meta.get('resources_order'))
855

    
856
    return render_response('im/resource_usage.html',
857
                           context_instance=get_context(request),
858
                           resource_catalog=resource_catalog,
859
                           resource_groups=resource_groups,
860
                           resources_order=resources_order,
861
                           current_usage=current_usage,
862
                           token_cookie_name=settings.COOKIE_NAME,
863
                           usage_update_interval=
864
                           settings.USAGE_UPDATE_INTERVAL)
865

    
866

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

    
877
    if provider.get_remove_policy:
878
        messages.success(request, provider.get_removed_msg)
879
        provider.remove_from_user()
880
        return HttpResponseRedirect(reverse('edit_profile'))
881
    else:
882
        raise PermissionDenied
883

    
884

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

    
896

    
897
@cookie_fix
898
def get_menu(request, with_extra_links=False, with_signout=True):
899
    user = request.user
900
    index_url = reverse('index')
901

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

    
921
        if with_extra_links:
922
            if settings.INVITATIONS_ENABLED:
923
                append(item(url=request.build_absolute_uri(reverse('invite')),
924
                            name="Invitations"))
925

    
926
            append(item(url=request.build_absolute_uri(reverse('api_access')),
927
                        name="API access"))
928

    
929
            append(
930
                item(
931
                    url=request.build_absolute_uri(reverse('resource_usage')),
932
                    name="Usage"))
933

    
934
            if settings.PROJECTS_VISIBLE:
935
                append(
936
                    item(
937
                        url=request.build_absolute_uri(
938
                            reverse('project_list')),
939
                        name="Projects"))
940

    
941
            append(item(url=request.build_absolute_uri(reverse('feedback')),
942
                        name="Contact"))
943
        if with_signout:
944
            append(item(url=request.build_absolute_uri(reverse('logout')),
945
                        name="Sign out"))
946
    else:
947
        l = [{'url': request.build_absolute_uri(index_url),
948
              'name': _("Sign in")}]
949

    
950
    callback = request.GET.get('callback', None)
951
    data = json.dumps(tuple(l))
952
    mimetype = 'application/json'
953

    
954
    if callback:
955
        mimetype = 'application/javascript'
956
        data = '%s(%s)' % (callback, data)
957

    
958
    return HttpResponse(content=data, mimetype=mimetype)
959

    
960

    
961
class MenuItem(dict):
962
    current_path = ''
963

    
964
    def __init__(self, *args, **kwargs):
965
        super(MenuItem, self).__init__(*args, **kwargs)
966
        if kwargs.get('url') or kwargs.get('submenu'):
967
            self.__set_is_active__()
968

    
969
    def __setitem__(self, key, value):
970
        super(MenuItem, self).__setitem__(key, value)
971
        if key in ('url', 'submenu'):
972
            self.__set_is_active__()
973

    
974
    def __set_is_active__(self):
975
        if self.get('is_active'):
976
            return
977
        if self.current_path.startswith(self.get('url')):
978
            self.__setitem__('is_active', True)
979
        else:
980
            submenu = self.get('submenu', ())
981
            current = (i for i in submenu if i.get('url') == self.current_path)
982
            try:
983
                current_node = current.next()
984
                if not current_node.get('is_active'):
985
                    current_node.__setitem__('is_active', True)
986
                self.__setitem__('is_active', True)
987
            except StopIteration:
988
                return
989

    
990
    def __setattribute__(self, name, value):
991
        super(MenuItem, self).__setattribute__(name, value)
992
        if name == 'current_path':
993
            self.__set_is_active__()
994

    
995

    
996
def get_services(request):
997
    callback = request.GET.get('callback', None)
998
    mimetype = 'application/json'
999
    data = json.dumps(Component.catalog().values())
1000

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

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