Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.6 kB)

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

    
34
import logging
35
import socket
36

    
37
from smtplib import SMTPException
38
from urllib import quote
39
from functools import wraps
40

    
41
from django.core.mail import send_mail
42
from django.http import HttpResponse, HttpResponseBadRequest
43
from django.shortcuts import redirect
44
from django.template.loader import render_to_string
45
from django.utils.translation import ugettext as _
46
from django.core.urlresolvers import reverse
47
from django.contrib.auth.decorators import login_required
48
from django.contrib import messages
49
from django.db import transaction
50
from django.utils.http import urlencode
51
from django.http import HttpResponseRedirect, HttpResponseBadRequest
52
from django.db.utils import IntegrityError
53
from django.contrib.auth.views import password_change
54
from django.core.exceptions import ValidationError
55
from django.db.models import Q
56
from django.views.decorators.http import require_http_methods
57

    
58
from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
59
from astakos.im.activation_backends import get_backend, SimpleBackend
60
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
61
from astakos.im.forms import *
62
from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
63
    invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
64
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
65

    
66
logger = logging.getLogger(__name__)
67

    
68
def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
69
    """
70
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
71
    keyword argument and returns an ``django.http.HttpResponse`` with the
72
    specified ``status``.
73
    """
74
    if tab is None:
75
        tab = template.partition('_')[0].partition('.html')[0]
76
    kwargs.setdefault('tab', tab)
77
    html = render_to_string(template, kwargs, context_instance=context_instance)
78
    response = HttpResponse(html, status=status)
79
    if reset_cookie:
80
        set_cookie(response, context_instance['request'].user)
81
    return response
82

    
83

    
84
def requires_anonymous(func):
85
    """
86
    Decorator checkes whether the request.user is not Anonymous and in that case
87
    redirects to `logout`.
88
    """
89
    @wraps(func)
90
    def wrapper(request, *args):
91
        if not request.user.is_anonymous():
92
            next = urlencode({'next': request.build_absolute_uri()})
93
            logout_uri = reverse(logout) + '?' + next
94
            return HttpResponseRedirect(logout_uri)
95
        return func(request, *args)
96
    return wrapper
97

    
98
def signed_terms_required(func):
99
    """
100
    Decorator checkes whether the request.user is Anonymous and in that case
101
    redirects to `logout`.
102
    """
103
    @wraps(func)
104
    def wrapper(request, *args, **kwargs):
105
        if request.user.is_authenticated() and not request.user.signed_terms():
106
            params = urlencode({'next': request.build_absolute_uri(),
107
                              'show_form':''})
108
            terms_uri = reverse('latest_terms') + '?' + params
109
            return HttpResponseRedirect(terms_uri)
110
        return func(request, *args, **kwargs)
111
    return wrapper
112

    
113
@require_http_methods(["GET", "POST"])
114
@signed_terms_required
115
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
116
    """
117
    If there is logged on user renders the profile page otherwise renders login page.
118

119
    **Arguments**
120

121
    ``login_template_name``
122
        A custom login template to use. This is optional; if not specified,
123
        this will default to ``im/login.html``.
124

125
    ``profile_template_name``
126
        A custom profile template to use. This is optional; if not specified,
127
        this will default to ``im/profile.html``.
128

129
    ``extra_context``
130
        An dictionary of variables to add to the template context.
131

132
    **Template:**
133

134
    im/profile.html or im/login.html or ``template_name`` keyword argument.
135

136
    """
137
    template_name = login_template_name
138
    if request.user.is_authenticated():
139
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
140
    return render_response(template_name,
141
                           login_form = LoginForm(request=request),
142
                           context_instance = get_context(request, extra_context))
143

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

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

156
    The view uses commit_manually decorator in order to ensure the number of the
157
    user invitations is going to be updated only if the email has been successfully sent.
158

159
    If the user isn't logged in, redirects to settings.LOGIN_URL.
160

161
    **Arguments**
162

163
    ``template_name``
164
        A custom template to use. This is optional; if not specified,
165
        this will default to ``im/invitations.html``.
166

167
    ``extra_context``
168
        An dictionary of variables to add to the template context.
169

170
    **Template:**
171

172
    im/invitations.html or ``template_name`` keyword argument.
173

174
    **Settings:**
175

176
    The view expectes the following settings are defined:
177

178
    * LOGIN_URL: login uri
179
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
180
    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
181
    """
182
    status = None
183
    message = None
184
    form = InvitationForm()
185
    
186
    inviter = request.user
187
    if request.method == 'POST':
188
        form = InvitationForm(request.POST)
189
        if inviter.invitations > 0:
190
            if form.is_valid():
191
                try:
192
                    invitation = form.save()
193
                    invite_func(invitation, inviter)
194
                    status = messages.SUCCESS
195
                    message = _('Invitation sent to %s' % invitation.username)
196
                except SendMailError, e:
197
                    status = messages.ERROR
198
                    message = e.message
199
                    transaction.rollback()
200
                except BaseException, e:
201
                    status = messages.ERROR
202
                    message = _('Something went wrong.')
203
                    logger.exception(e)
204
                    transaction.rollback()
205
                else:
206
                    transaction.commit()
207
        else:
208
            status = messages.ERROR
209
            message = _('No invitations left')
210
    messages.add_message(request, status, message)
211

    
212
    sent = [{'email': inv.username,
213
             'realname': inv.realname,
214
             'is_consumed': inv.is_consumed}
215
             for inv in request.user.invitations_sent.all()]
216
    kwargs = {'inviter': inviter,
217
              'sent':sent}
218
    context = get_context(request, extra_context, **kwargs)
219
    return render_response(template_name,
220
                           invitation_form = form,
221
                           context_instance = context)
222

    
223
@require_http_methods(["GET", "POST"])
224
@login_required
225
@signed_terms_required
226
def edit_profile(request, template_name='im/profile.html', extra_context={}):
227
    """
228
    Allows a user to edit his/her profile.
229

230
    In case of GET request renders a form for displaying the user information.
231
    In case of POST updates the user informantion and redirects to ``next``
232
    url parameter if exists.
233

234
    If the user isn't logged in, redirects to settings.LOGIN_URL.
235

236
    **Arguments**
237

238
    ``template_name``
239
        A custom template to use. This is optional; if not specified,
240
        this will default to ``im/profile.html``.
241

242
    ``extra_context``
243
        An dictionary of variables to add to the template context.
244

245
    **Template:**
246

247
    im/profile.html or ``template_name`` keyword argument.
248

249
    **Settings:**
250

251
    The view expectes the following settings are defined:
252

253
    * LOGIN_URL: login uri
254
    """
255
    form = ProfileForm(instance=request.user)
256
    extra_context['next'] = request.GET.get('next')
257
    reset_cookie = False
258
    if request.method == 'POST':
259
        form = ProfileForm(request.POST, instance=request.user)
260
        if form.is_valid():
261
            try:
262
                prev_token = request.user.auth_token
263
                user = form.save()
264
                reset_cookie = user.auth_token != prev_token
265
                form = ProfileForm(instance=user)
266
                next = request.POST.get('next')
267
                if next:
268
                    return redirect(next)
269
                msg = _('<p>Profile has been updated successfully</p>')
270
                messages.add_message(request, messages.SUCCESS, msg)
271
            except ValueError, ve:
272
                messages.add_message(request, messages.ERROR, ve)
273
    elif request.method == "GET":
274
        request.user.is_verified = True
275
        request.user.save()
276
    return render_response(template_name,
277
                           reset_cookie = reset_cookie,
278
                           profile_form = form,
279
                           context_instance = get_context(request,
280
                                                          extra_context))
281

    
282
@require_http_methods(["GET", "POST"])
283
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
284
    """
285
    Allows a user to create a local account.
286

287
    In case of GET request renders a form for entering the user information.
288
    In case of POST handles the signup.
289

290
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
291
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
292
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
293
    (see activation_backends);
294
    
295
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
296
    otherwise renders the same page with a success message.
297
    
298
    On unsuccessful creation, renders ``template_name`` with an error message.
299
    
300
    **Arguments**
301
    
302
    ``template_name``
303
        A custom template to render. This is optional;
304
        if not specified, this will default to ``im/signup.html``.
305

306
    ``on_success``
307
        A custom template to render in case of success. This is optional;
308
        if not specified, this will default to ``im/signup_complete.html``.
309

310
    ``extra_context``
311
        An dictionary of variables to add to the template context.
312

313
    **Template:**
314
    
315
    im/signup.html or ``template_name`` keyword argument.
316
    im/signup_complete.html or ``on_success`` keyword argument. 
317
    """
318
    if request.user.is_authenticated():
319
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
320
    
321
    provider = get_query(request).get('provider', 'local')
322
    try:
323
        if not backend:
324
            backend = get_backend(request)
325
        form = backend.get_signup_form(provider)
326
    except Exception, e:
327
        form = SimpleBackend(request).get_signup_form(provider)
328
        messages.add_message(request, messages.ERROR, e)
329
    if request.method == 'POST':
330
        if form.is_valid():
331
            user = form.save(commit=False)
332
            try:
333
                result = backend.handle_activation(user)
334
                status = messages.SUCCESS
335
                message = result.message
336
                user.save()
337
                if 'additional_email' in form.cleaned_data:
338
                    additional_email = form.cleaned_data['additional_email']
339
                    if additional_email != user.email:
340
                        user.additionalmail_set.create(email=additional_email)
341
                        msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
342
                        logger._log(LOGGING_LEVEL, msg, [])
343
                if user and user.is_active:
344
                    next = request.POST.get('next', '')
345
                    return prepare_response(request, user, next=next)
346
                messages.add_message(request, status, message)
347
                return render_response(on_success,
348
                                       context_instance=get_context(request, extra_context))
349
            except SendMailError, e:
350
                status = messages.ERROR
351
                message = e.message
352
                messages.add_message(request, status, message)
353
            except BaseException, e:
354
                status = messages.ERROR
355
                message = _('Something went wrong.')
356
                messages.add_message(request, status, message)
357
                logger.exception(e)
358
    return render_response(template_name,
359
                           signup_form = form,
360
                           provider = provider,
361
                           context_instance=get_context(request, extra_context))
362

    
363
@require_http_methods(["GET", "POST"])
364
@login_required
365
@signed_terms_required
366
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
367
    """
368
    Allows a user to send feedback.
369

370
    In case of GET request renders a form for providing the feedback information.
371
    In case of POST sends an email to support team.
372

373
    If the user isn't logged in, redirects to settings.LOGIN_URL.
374

375
    **Arguments**
376

377
    ``template_name``
378
        A custom template to use. This is optional; if not specified,
379
        this will default to ``im/feedback.html``.
380

381
    ``extra_context``
382
        An dictionary of variables to add to the template context.
383

384
    **Template:**
385

386
    im/signup.html or ``template_name`` keyword argument.
387

388
    **Settings:**
389

390
    * LOGIN_URL: login uri
391
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
392
    """
393
    if request.method == 'GET':
394
        form = FeedbackForm()
395
    if request.method == 'POST':
396
        if not request.user:
397
            return HttpResponse('Unauthorized', status=401)
398

    
399
        form = FeedbackForm(request.POST)
400
        if form.is_valid():
401
            msg = form.cleaned_data['feedback_msg']
402
            data = form.cleaned_data['feedback_data']
403
            try:
404
                send_feedback(msg, data, request.user, email_template_name)
405
            except SendMailError, e:
406
                message = e.message
407
                status = messages.ERROR
408
            else:
409
                message = _('Feedback successfully sent')
410
                status = messages.SUCCESS
411
            messages.add_message(request, status, message)
412
    return render_response(template_name,
413
                           feedback_form = form,
414
                           context_instance = get_context(request, extra_context))
415

    
416
@require_http_methods(["GET", "POST"])
417
def logout(request, template='registration/logged_out.html', extra_context={}):
418
    """
419
    Wraps `django.contrib.auth.logout` and delete the cookie.
420
    """
421
    response = HttpResponse()
422
    if request.user.is_authenticated():
423
        email = request.user.email
424
        auth_logout(request)
425
        response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
426
        msg = 'Cookie deleted for %s' % email
427
        logger._log(LOGGING_LEVEL, msg, [])
428
    next = request.GET.get('next')
429
    if next:
430
        response['Location'] = next
431
        response.status_code = 302
432
        return response
433
    elif LOGOUT_NEXT:
434
        response['Location'] = LOGOUT_NEXT
435
        response.status_code = 301
436
        return response
437
    messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
438
    context = get_context(request, extra_context)
439
    response.write(render_to_string(template, context_instance=context))
440
    return response
441

    
442
@require_http_methods(["GET", "POST"])
443
@transaction.commit_manually
444
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
445
    """
446
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
447
    and renews the user token.
448

449
    The view uses commit_manually decorator in order to ensure the user state will be updated
450
    only if the email will be send successfully.
451
    """
452
    token = request.GET.get('auth')
453
    next = request.GET.get('next')
454
    try:
455
        user = AstakosUser.objects.get(auth_token=token)
456
    except AstakosUser.DoesNotExist:
457
        return HttpResponseBadRequest(_('No such user'))
458
    
459
    if user.is_active:
460
        message = _('Account already active.')
461
        messages.add_message(request, messages.ERROR, message)
462
        return index(request)
463
    
464
    try:
465
        local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
466
    except AstakosUser.DoesNotExist:
467
        try:
468
            activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
469
            response = prepare_response(request, user, next, renew=True)
470
            transaction.commit()
471
            return response
472
        except SendMailError, e:
473
            message = e.message
474
            messages.add_message(request, messages.ERROR, message)
475
            transaction.rollback()
476
            return index(request)
477
        except BaseException, e:
478
            status = messages.ERROR
479
            message = _('Something went wrong.')
480
            messages.add_message(request, messages.ERROR, message)
481
            logger.exception(e)
482
            transaction.rollback()
483
            return index(request)
484
    else:
485
        try:
486
            user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
487
            response = prepare_response(request, user, next, renew=True)
488
            transaction.commit()
489
            return response
490
        except SendMailError, e:
491
            message = e.message
492
            messages.add_message(request, messages.ERROR, message)
493
            transaction.rollback()
494
            return index(request)
495
        except BaseException, e:
496
            status = messages.ERROR
497
            message = _('Something went wrong.')
498
            messages.add_message(request, messages.ERROR, message)
499
            logger.exception(e)
500
            transaction.rollback()
501
            return index(request)
502

    
503
@require_http_methods(["GET", "POST"])
504
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
505
    term = None
506
    terms = None
507
    if not term_id:
508
        try:
509
            term = ApprovalTerms.objects.order_by('-id')[0]
510
        except IndexError:
511
            pass
512
    else:
513
        try:
514
             term = ApprovalTerms.objects.get(id=term_id)
515
        except ApprovalTermDoesNotExist, e:
516
            pass
517

    
518
    if not term:
519
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
520
    f = open(term.location, 'r')
521
    terms = f.read()
522

    
523
    if request.method == 'POST':
524
        next = request.POST.get('next')
525
        if not next:
526
            next = reverse('astakos.im.views.index')
527
        form = SignApprovalTermsForm(request.POST, instance=request.user)
528
        if not form.is_valid():
529
            return render_response(template_name,
530
                           terms = terms,
531
                           approval_terms_form = form,
532
                           context_instance = get_context(request, extra_context))
533
        user = form.save()
534
        return HttpResponseRedirect(next)
535
    else:
536
        form = None
537
        if request.user.is_authenticated() and not request.user.signed_terms():
538
            form = SignApprovalTermsForm(instance=request.user)
539
        return render_response(template_name,
540
                               terms = terms,
541
                               approval_terms_form = form,
542
                               context_instance = get_context(request, extra_context))
543

    
544
@require_http_methods(["GET", "POST"])
545
@signed_terms_required
546
def change_password(request):
547
    return password_change(request,
548
                            post_change_redirect=reverse('astakos.im.views.edit_profile'),
549
                            password_change_form=ExtendedPasswordChangeForm)
550

    
551
@require_http_methods(["GET", "POST"])
552
@login_required
553
@signed_terms_required
554
@transaction.commit_manually
555
def change_email(request, activation_key=None,
556
                 email_template_name='registration/email_change_email.txt',
557
                 form_template_name='registration/email_change_form.html',
558
                 confirm_template_name='registration/email_change_done.html',
559
                 extra_context={}):
560
    if activation_key:
561
        try:
562
            user = EmailChange.objects.change_email(activation_key)
563
            if request.user.is_authenticated() and request.user == user:
564
                msg = _('Email changed successfully.')
565
                messages.add_message(request, messages.SUCCESS, msg)
566
                auth_logout(request)
567
                response = prepare_response(request, user)
568
                transaction.commit()
569
                return response
570
        except ValueError, e:
571
            messages.add_message(request, messages.ERROR, e)
572
        return render_response(confirm_template_name,
573
                               modified_user = user if 'user' in locals() else None,
574
                               context_instance = get_context(request,
575
                                                              extra_context))
576
    
577
    if not request.user.is_authenticated():
578
        path = quote(request.get_full_path())
579
        url = request.build_absolute_uri(reverse('astakos.im.views.index'))
580
        return HttpResponseRedirect(url + '?next=' + path)
581
    form = EmailChangeForm(request.POST or None)
582
    if request.method == 'POST' and form.is_valid():
583
        try:
584
            ec = form.save(email_template_name, request)
585
        except SendMailError, e:
586
            status = messages.ERROR
587
            msg = e
588
            transaction.rollback()
589
        except IntegrityError, e:
590
            status = messages.ERROR
591
            msg = _('There is already a pending change email request.')
592
        else:
593
            status = messages.SUCCESS
594
            msg = _('Change email request has been registered succefully.\
595
                    You are going to receive a verification email in the new address.')
596
            transaction.commit()
597
        messages.add_message(request, status, msg)
598
    return render_response(form_template_name,
599
                           form = form,
600
                           context_instance = get_context(request,
601
                                                          extra_context))