Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.4 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
63
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT
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 providing 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

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

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

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

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

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

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

368
    **Arguments**
369

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

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

377
    **Template:**
378

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

381
    **Settings:**
382

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

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

    
409
def logout(request, template='registration/logged_out.html', extra_context={}):
410
    """
411
    Wraps `django.contrib.auth.logout` and delete the cookie.
412
    """
413
    auth_logout(request)
414
    response = HttpResponse()
415
    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
416
    next = request.GET.get('next')
417
    if next:
418
        response['Location'] = next
419
        response.status_code = 302
420
        return response
421
    elif LOGOUT_NEXT:
422
        response['Location'] = LOGOUT_NEXT
423
        response.status_code = 301
424
        return response
425
    messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
426
    context = get_context(request, extra_context)
427
    response.write(render_to_string(template, context_instance=context))
428
    return response
429

    
430
@transaction.commit_manually
431
def activate(request, email_template_name='im/welcome_email.txt', on_failure='im/signup.html'):
432
    """
433
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
434
    and renews the user token.
435

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

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

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

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

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

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