Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.1 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

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

    
65
logger = logging.getLogger(__name__)
66

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

    
82

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

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

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

117
    **Arguments**
118

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

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

127
    ``extra_context``
128
        An dictionary of variables to add to the template context.
129

130
    **Template:**
131

132
    im/profile.html or im/login.html or ``template_name`` keyword argument.
133

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

    
142
@login_required
143
@signed_terms_required
144
@transaction.commit_manually
145
def invite(request, template_name='im/invitations.html', extra_context={}):
146
    """
147
    Allows a user to invite somebody else.
148

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

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

156
    If the user isn't logged in, redirects to settings.LOGIN_URL.
157

158
    **Arguments**
159

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

164
    ``extra_context``
165
        An dictionary of variables to add to the template context.
166

167
    **Template:**
168

169
    im/invitations.html or ``template_name`` keyword argument.
170

171
    **Settings:**
172

173
    The view expectes the following settings are defined:
174

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

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

    
220
@login_required
221
@signed_terms_required
222
def edit_profile(request, template_name='im/profile.html', extra_context={}):
223
    """
224
    Allows a user to edit his/her profile.
225

226
    In case of GET request renders a form for displaying the user information.
227
    In case of POST updates the user informantion and redirects to ``next``
228
    url parameter if exists.
229

230
    If the user isn't logged in, redirects to settings.LOGIN_URL.
231

232
    **Arguments**
233

234
    ``template_name``
235
        A custom template to use. This is optional; if not specified,
236
        this will default to ``im/profile.html``.
237

238
    ``extra_context``
239
        An dictionary of variables to add to the template context.
240

241
    **Template:**
242

243
    im/profile.html or ``template_name`` keyword argument.
244

245
    **Settings:**
246

247
    The view expectes the following settings are defined:
248

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

    
278
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
279
    """
280
    Allows a user to create a local account.
281

282
    In case of GET request renders a form for entering the user information.
283
    In case of POST handles the signup.
284

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

301
    ``on_success``
302
        A custom template to render in case of success. This is optional;
303
        if not specified, this will default to ``im/signup_complete.html``.
304

305
    ``extra_context``
306
        An dictionary of variables to add to the template context.
307

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

    
358
@login_required
359
@signed_terms_required
360
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
361
    """
362
    Allows a user to send feedback.
363

364
    In case of GET request renders a form for providing the feedback information.
365
    In case of POST sends an email to support team.
366

367
    If the user isn't logged in, redirects to settings.LOGIN_URL.
368

369
    **Arguments**
370

371
    ``template_name``
372
        A custom template to use. This is optional; if not specified,
373
        this will default to ``im/feedback.html``.
374

375
    ``extra_context``
376
        An dictionary of variables to add to the template context.
377

378
    **Template:**
379

380
    im/signup.html or ``template_name`` keyword argument.
381

382
    **Settings:**
383

384
    * LOGIN_URL: login uri
385
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
386
    """
387
    if request.method == 'GET':
388
        form = FeedbackForm()
389
    if request.method == 'POST':
390
        if not request.user:
391
            return HttpResponse('Unauthorized', status=401)
392

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

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

    
435
@transaction.commit_manually
436
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
437
    """
438
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
439
    and renews the user token.
440

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

    
495
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
496
    term = None
497
    terms = None
498
    if not term_id:
499
        try:
500
            term = ApprovalTerms.objects.order_by('-id')[0]
501
        except IndexError:
502
            pass
503
    else:
504
        try:
505
             term = ApprovalTerms.objects.get(id=term_id)
506
        except ApprovalTermDoesNotExist, e:
507
            pass
508

    
509
    if not term:
510
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
511
    f = open(term.location, 'r')
512
    terms = f.read()
513

    
514
    if request.method == 'POST':
515
        next = request.POST.get('next')
516
        if not next:
517
            next = reverse('astakos.im.views.index')
518
        form = SignApprovalTermsForm(request.POST, instance=request.user)
519
        if not form.is_valid():
520
            return render_response(template_name,
521
                           terms = terms,
522
                           approval_terms_form = form,
523
                           context_instance = get_context(request, extra_context))
524
        user = form.save()
525
        return HttpResponseRedirect(next)
526
    else:
527
        form = None
528
        if request.user.is_authenticated() and not request.user.signed_terms():
529
            form = SignApprovalTermsForm(instance=request.user)
530
        return render_response(template_name,
531
                               terms = terms,
532
                               approval_terms_form = form,
533
                               context_instance = get_context(request, extra_context))
534

    
535
@signed_terms_required
536
def change_password(request):
537
    return password_change(request,
538
                            post_change_redirect=reverse('astakos.im.views.edit_profile'),
539
                            password_change_form=ExtendedPasswordChangeForm)
540

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