Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 142e3647

History | View | Annotate | Download (22.9 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 = _('Profile has been updated successfully')
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
    msg = 'Cookie deleted for %s' % (request.user.email)
415
    auth_logout(request)
416
    response = HttpResponse()
417
    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
418
    logger._log(LOGGING_LEVEL, msg, [])
419
    next = request.GET.get('next')
420
    if next:
421
        response['Location'] = next
422
        response.status_code = 302
423
        return response
424
    elif LOGOUT_NEXT:
425
        response['Location'] = LOGOUT_NEXT
426
        response.status_code = 301
427
        return response
428
    messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
429
    context = get_context(request, extra_context)
430
    response.write(render_to_string(template, context_instance=context))
431
    return response
432

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

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

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

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

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

    
533
@signed_terms_required
534
def change_password(request):
535
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
536

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