Statistics
| Branch: | Tag: | Revision:

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

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, send_helpdesk_notification
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', on_failure='im/signup.html', 
435
                helpdesk_email_template_name='im/helpdesk_notification.txt'):
436
    """
437
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
438
    and renews the user token.
439

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

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

    
513
    if not term:
514
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
515
    f = open(term.location, 'r')
516
    terms = f.read()
517

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

    
539
@signed_terms_required
540
def change_password(request):
541
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
542

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