Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.7 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
    if not auth.get_provider(provider).get_create_policy:
413
        logger.error("%s provider not available for signup", provider)
414
        raise PermissionDenied
415

    
416
    instance = None
417

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

    
428
        provider = pending.provider
429

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

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

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

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

    
451
    form_kwargs = {'instance': instance, 'request': request}
452
    if third_party_token:
453
        form_kwargs['third_party_token'] = third_party_token
454

    
455
    if pending:
456
        form_kwargs['initial'] = {
457
            'first_name': pending.first_name,
458
            'last_name': pending.last_name,
459
            'email': pending.email
460
        }
461

    
462
    form = activation_backend.get_signup_form(
463
        provider, None, **form_kwargs)
464

    
465
    if request.method == 'POST':
466
        form = activation_backend.get_signup_form(
467
            provider,
468
            request.POST,
469
            **form_kwargs)
470

    
471
        if form.is_valid():
472
            user = form.create_user()
473
            result = activation_backend.handle_registration(user)
474
            if result.status == \
475
                    activation_backend.Result.PENDING_MODERATION:
476
                # user should be warned that his account is not active yet
477
                status = messages.WARNING
478
            else:
479
                status = messages.SUCCESS
480
            message = result.message
481
            activation_backend.send_result_notifications(result, user)
482

    
483
            # commit user entry
484
            transaction.commit()
485

    
486
            if user and user.is_active:
487
                # activation backend directly activated the user
488
                # log him in
489
                next = request.POST.get('next', '')
490
                response = prepare_response(request, user, next=next)
491
                return response
492

    
493
            messages.add_message(request, status, message)
494
            return HttpResponseRedirect(reverse(on_success))
495

    
496
    return render_response(
497
        template_name,
498
        signup_form=form,
499
        third_party_token=third_party_token,
500
        provider=provider,
501
        context_instance=get_context(request, extra_context))
502

    
503

    
504
@require_http_methods(["GET", "POST"])
505
@required_auth_methods_assigned(allow_access=True)
506
@login_required
507
@cookie_fix
508
@signed_terms_required
509
def feedback(request, template_name='im/feedback.html',
510
             email_template_name='im/feedback_mail.txt', extra_context=None):
511
    """
512
    Allows a user to send feedback.
513

514
    In case of GET request renders a form for providing the feedback
515
    information.
516
    In case of POST sends an email to support team.
517

518
    If the user isn't logged in, redirects to settings.LOGIN_URL.
519

520
    **Arguments**
521

522
    ``template_name``
523
        A custom template to use. This is optional; if not specified,
524
        this will default to ``im/feedback.html``.
525

526
    ``extra_context``
527
        An dictionary of variables to add to the template context.
528

529
    **Template:**
530

531
    im/signup.html or ``template_name`` keyword argument.
532

533
    **Settings:**
534

535
    * LOGIN_URL: login uri
536
    """
537
    extra_context = extra_context or {}
538
    if request.method == 'GET':
539
        form = FeedbackForm()
540
    if request.method == 'POST':
541
        if not request.user:
542
            return HttpResponse('Unauthorized', status=401)
543

    
544
        form = FeedbackForm(request.POST)
545
        if form.is_valid():
546
            msg = form.cleaned_data['feedback_msg']
547
            data = form.cleaned_data['feedback_data']
548
            send_feedback(msg, data, request.user, email_template_name)
549
            message = _(astakos_messages.FEEDBACK_SENT)
550
            messages.success(request, message)
551
            return HttpResponseRedirect(reverse('feedback'))
552

    
553
    return render_response(template_name,
554
                           feedback_form=form,
555
                           context_instance=get_context(request,
556
                                                        extra_context))
557

    
558

    
559
@require_http_methods(["GET"])
560
@cookie_fix
561
def logout(request, template='registration/logged_out.html',
562
           extra_context=None):
563
    """
564
    Wraps `django.contrib.auth.logout`.
565
    """
566
    extra_context = extra_context or {}
567
    response = HttpResponse()
568
    if request.user.is_authenticated():
569
        email = request.user.email
570
        auth_logout(request)
571
    else:
572
        response['Location'] = reverse('index')
573
        response.status_code = 301
574
        return response
575

    
576
    next = restrict_next(
577
        request.GET.get('next'),
578
        domain=settings.COOKIE_DOMAIN
579
    )
580

    
581
    if next:
582
        response['Location'] = next
583
        response.status_code = 302
584
    elif settings.LOGOUT_NEXT:
585
        response['Location'] = settings.LOGOUT_NEXT
586
        response.status_code = 301
587
    else:
588
        last_provider = request.COOKIES.get(
589
            'astakos_last_login_method', 'local')
590
        provider = auth.get_provider(last_provider)
591
        message = provider.get_logout_success_msg
592
        extra = provider.get_logout_success_extra_msg
593
        if extra:
594
            message += "<br />" + extra
595
        messages.success(request, message)
596
        response['Location'] = reverse('index')
597
        response.status_code = 301
598
    return response
599

    
600

    
601
@require_http_methods(["GET", "POST"])
602
@cookie_fix
603
@transaction.commit_on_success
604
def activate(request, greeting_email_template_name='im/welcome_email.txt',
605
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
606
    """
607
    Activates the user identified by the ``auth`` request parameter, sends a
608
    welcome email and renews the user token.
609

610
    The user state will be updated only if the email will be send successfully.
611
    """
612
    token = request.GET.get('auth', None)
613
    next = request.GET.get('next', None)
614

    
615
    if not token:
616
        raise PermissionDenied
617

    
618
    if request.user.is_authenticated():
619
        message = _(astakos_messages.LOGGED_IN_WARNING)
620
        messages.error(request, message)
621
        return HttpResponseRedirect(reverse('index'))
622

    
623
    try:
624
        user = AstakosUser.objects.select_for_update().\
625
            get(verification_code=token)
626
    except AstakosUser.DoesNotExist:
627
        raise Http404
628

    
629
    if user.email_verified:
630
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
631
        messages.error(request, message)
632
        return HttpResponseRedirect(reverse('index'))
633

    
634
    backend = activation_backends.get_backend()
635
    result = backend.handle_verification(user, token)
636
    backend.send_result_notifications(result, user)
637
    next = settings.ACTIVATION_REDIRECT_URL or next or reverse('index')
638
    if user.is_active:
639
        response = prepare_response(request, user, next, renew=True)
640
        messages.success(request, _(result.message))
641
    else:
642
        response = HttpResponseRedirect(reverse('index'))
643
        messages.warning(request, _(result.message))
644

    
645
    return response
646

    
647

    
648
@login_required
649
def _approval_terms_post(request, template_name, terms, extra_context):
650
    next = restrict_next(
651
        request.POST.get('next'),
652
        domain=settings.COOKIE_DOMAIN
653
    )
654
    if not next:
655
        next = reverse('index')
656
    form = SignApprovalTermsForm(request.POST, instance=request.user)
657
    if not form.is_valid():
658
        return render_response(template_name,
659
                               terms=terms,
660
                               approval_terms_form=form,
661
                               context_instance=get_context(request,
662
                                                            extra_context))
663
    user = form.save()
664
    return HttpResponseRedirect(next)
665

    
666

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

    
685
    if not terms_record:
686
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
687
        return HttpResponseRedirect(reverse('index'))
688
    try:
689
        f = open(terms_record.location, 'r')
690
    except IOError:
691
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
692
        return render_response(
693
            template_name, context_instance=get_context(request,
694
                                                        extra_context))
695

    
696
    terms = f.read()
697

    
698
    if request.method == 'POST':
699
        return _approval_terms_post(request, template_name, terms,
700
                                    extra_context)
701
    else:
702
        form = None
703
        if request.user.is_authenticated() and not request.user.signed_terms:
704
            form = SignApprovalTermsForm(instance=request.user)
705
        return render_response(template_name,
706
                               terms=terms,
707
                               approval_terms_form=form,
708
                               context_instance=get_context(request,
709
                                                            extra_context))
710

    
711

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

    
722
    if not settings.EMAILCHANGE_ENABLED:
723
        raise PermissionDenied
724

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

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

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

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

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

    
773
    form = EmailChangeForm(request.POST or None)
774
    if request.method == 'POST' and form.is_valid():
775
        ec = form.save(request, email_template_name, request)
776
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
777
        messages.success(request, msg)
778
        transaction.commit()
779
        return HttpResponseRedirect(reverse('edit_profile'))
780

    
781
    if request.user.email_change_is_pending():
782
        messages.warning(request,
783
                         astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
784

    
785
    return render_response(
786
        form_template_name,
787
        form=form,
788
        context_instance=get_context(request, extra_context)
789
    )
790

    
791

    
792
@cookie_fix
793
@transaction.commit_on_success
794
def send_activation(request, user_id, template_name='im/login.html',
795
                    extra_context=None):
796

    
797
    if request.user.is_authenticated():
798
        return HttpResponseRedirect(reverse('index'))
799

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

    
810
            messages.error(request,
811
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
812
        else:
813
            activation_backend = activation_backends.get_backend()
814
            activation_backend.send_user_verification_email(u)
815
            messages.success(request, astakos_messages.ACTIVATION_SENT)
816

    
817
    return HttpResponseRedirect(reverse('index'))
818

    
819

    
820
@require_http_methods(["GET"])
821
@cookie_fix
822
@valid_astakos_user_required
823
def resource_usage(request):
824

    
825
    resources_meta = presentation.RESOURCES
826

    
827
    current_usage = quotas.get_user_quotas(request.user)
828
    current_usage = json.dumps(current_usage['system'])
829
    resource_catalog, resource_groups = _resources_catalog()
830
    if resource_catalog is False:
831
        # on fail resource_groups contains the result object
832
        result = resource_groups
833
        messages.error(request, 'Unable to retrieve system resources: %s' %
834
                       result.reason)
835

    
836
    resource_catalog = json.dumps(resource_catalog)
837
    resource_groups = json.dumps(resource_groups)
838
    resources_order = json.dumps(resources_meta.get('resources_order'))
839

    
840
    return render_response('im/resource_usage.html',
841
                           context_instance=get_context(request),
842
                           resource_catalog=resource_catalog,
843
                           resource_groups=resource_groups,
844
                           resources_order=resources_order,
845
                           current_usage=current_usage,
846
                           token_cookie_name=settings.COOKIE_NAME,
847
                           usage_update_interval=
848
                           settings.USAGE_UPDATE_INTERVAL)
849

    
850

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

    
861
    if provider.get_remove_policy:
862
        messages.success(request, provider.get_removed_msg)
863
        provider.remove_from_user()
864
        return HttpResponseRedirect(reverse('edit_profile'))
865
    else:
866
        raise PermissionDenied
867

    
868

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

    
880

    
881
@cookie_fix
882
def get_menu(request, with_extra_links=False, with_signout=True):
883
    user = request.user
884
    index_url = reverse('index')
885

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

    
905
        if with_extra_links:
906
            if settings.INVITATIONS_ENABLED:
907
                append(item(url=request.build_absolute_uri(reverse('invite')),
908
                            name="Invitations"))
909

    
910
            append(item(url=request.build_absolute_uri(reverse('api_access')),
911
                        name="API access"))
912

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

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

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

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

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

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

    
944

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

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

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

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

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

    
979

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

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

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