Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 95b7c3f6

History | View | Annotate | Download (34.8 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
        get_verified = AstakosUserAuthProvider.objects.verified
439
        verified = get_verified(pending.provider,
440
                                identifier=pending.third_party_identifier)
441
        if verified:
442
            # an existing verified user already exists for the third party
443
            # identifier
444
            pending.delete()
445
            raise Http404
446

    
447
        if unverified and request.method == 'GET':
448
            messages.warning(request, unverified.get_pending_registration_msg)
449

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

    
454
    form_kwargs = {'instance': instance, 'request': request}
455
    if third_party_token:
456
        form_kwargs['third_party_token'] = third_party_token
457

    
458
    if pending:
459
        form_kwargs['initial'] = {
460
            'first_name': pending.first_name,
461
            'last_name': pending.last_name,
462
            'email': pending.email
463
        }
464

    
465
    form = activation_backend.get_signup_form(
466
        provider, None, **form_kwargs)
467

    
468

    
469
    if request.method == 'POST':
470
        form = activation_backend.get_signup_form(
471
            provider,
472
            request.POST,
473
            **form_kwargs)
474

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

    
487
            # commit user entry
488
            transaction.commit()
489

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

    
497
            messages.add_message(request, status, message)
498
            return HttpResponseRedirect(reverse(on_success))
499

    
500
    return render_response(
501
        template_name,
502
        signup_form=form,
503
        third_party_token=third_party_token,
504
        provider=provider,
505
        context_instance=get_context(request, extra_context))
506

    
507

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

518
    In case of GET request renders a form for providing the feedback
519
    information.
520
    In case of POST sends an email to support team.
521

522
    If the user isn't logged in, redirects to settings.LOGIN_URL.
523

524
    **Arguments**
525

526
    ``template_name``
527
        A custom template to use. This is optional; if not specified,
528
        this will default to ``im/feedback.html``.
529

530
    ``extra_context``
531
        An dictionary of variables to add to the template context.
532

533
    **Template:**
534

535
    im/signup.html or ``template_name`` keyword argument.
536

537
    **Settings:**
538

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

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

    
557
    return render_response(template_name,
558
                           feedback_form=form,
559
                           context_instance=get_context(request,
560
                                                        extra_context))
561

    
562

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

    
580
    next = restrict_next(
581
        request.GET.get('next'),
582
        domain=settings.COOKIE_DOMAIN
583
    )
584

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

    
604

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

614
    The user state will be updated only if the email will be send successfully.
615
    """
616
    token = request.GET.get('auth', None)
617
    next = request.GET.get('next', None)
618

    
619
    if not token:
620
        raise PermissionDenied
621

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

    
627
    try:
628
        user = AstakosUser.objects.select_for_update().\
629
            get(verification_code=token)
630
    except AstakosUser.DoesNotExist:
631
        messages.error(request, astakos_messages.INVALID_ACTIVATION_KEY)
632
        return HttpResponseRedirect(reverse('index'))
633

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

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

    
650
    return response
651

    
652

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

    
671

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

    
690
    if not terms_record:
691
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
692
        return HttpResponseRedirect(reverse('index'))
693
    try:
694
        f = open(terms_record.location, 'r')
695
    except IOError:
696
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
697
        return render_response(
698
            template_name, context_instance=get_context(request,
699
                                                        extra_context))
700

    
701
    terms = f.read()
702

    
703
    if request.method == 'POST':
704
        return _approval_terms_post(request, template_name, terms,
705
                                    extra_context)
706
    else:
707
        form = None
708
        if request.user.is_authenticated() and not request.user.signed_terms:
709
            form = SignApprovalTermsForm(instance=request.user)
710
        return render_response(template_name,
711
                               terms=terms,
712
                               approval_terms_form=form,
713
                               context_instance=get_context(request,
714
                                                            extra_context))
715

    
716

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

    
727
    if not settings.EMAILCHANGE_ENABLED:
728
        raise PermissionDenied
729

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

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

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

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

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

    
778
    form = EmailChangeForm(request.POST or None)
779
    if request.method == 'POST' and form.is_valid():
780
        ec = form.save(request, email_template_name, request)
781
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
782
        messages.success(request, msg)
783
        transaction.commit()
784
        return HttpResponseRedirect(reverse('edit_profile'))
785

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

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

    
796

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

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

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

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

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

    
824

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

    
830
    resources_meta = presentation.RESOURCES
831

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

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

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

    
855

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

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

    
873

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

    
885

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

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

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

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

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

    
923
            if settings.PROJECTS_VISIBLE:
924
                append(
925
                    item(
926
                        url=request.build_absolute_uri(
927
                            reverse('project_list')),
928
                        name="Projects"))
929

    
930
            append(item(url=request.build_absolute_uri(reverse('feedback')),
931
                        name="Contact"))
932
        if with_signout:
933
            append(item(url=request.build_absolute_uri(reverse('logout')),
934
                        name="Sign out"))
935
    else:
936
        l = [{'url': request.build_absolute_uri(index_url),
937
              'name': _("Sign in")}]
938

    
939
    callback = request.GET.get('callback', None)
940
    data = json.dumps(tuple(l))
941
    mimetype = 'application/json'
942

    
943
    if callback:
944
        mimetype = 'application/javascript'
945
        data = '%s(%s)' % (callback, data)
946

    
947
    return HttpResponse(content=data, mimetype=mimetype)
948

    
949

    
950
class MenuItem(dict):
951
    current_path = ''
952

    
953
    def __init__(self, *args, **kwargs):
954
        super(MenuItem, self).__init__(*args, **kwargs)
955
        if kwargs.get('url') or kwargs.get('submenu'):
956
            self.__set_is_active__()
957

    
958
    def __setitem__(self, key, value):
959
        super(MenuItem, self).__setitem__(key, value)
960
        if key in ('url', 'submenu'):
961
            self.__set_is_active__()
962

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

    
979
    def __setattribute__(self, name, value):
980
        super(MenuItem, self).__setattribute__(name, value)
981
        if name == 'current_path':
982
            self.__set_is_active__()
983

    
984

    
985
def get_services(request):
986
    callback = request.GET.get('callback', None)
987
    mimetype = 'application/json'
988
    data = json.dumps(Component.catalog().values())
989

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

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