Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 27e26a41

History | View | Annotate | Download (19.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.contrib.auth import logout as auth_logout
51
from django.utils.http import urlencode
52
from django.http import HttpResponseRedirect, HttpResponseBadRequest
53
from django.db.utils import IntegrityError
54
from django.contrib.auth.views import password_change
55
from django.core.exceptions import ValidationError
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
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT
63
from astakos.im.functions import invite as invite_func
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
    return render_response(template_name,
270
                           reset_cookie = reset_cookie,
271
                           profile_form = form,
272
                           context_instance = get_context(request,
273
                                                          extra_context))
274

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

279
    In case of GET request renders a form for providing the user information.
280
    In case of POST handles the signup.
281

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

298

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

303
    ``extra_context``
304
        An dictionary of variables to add to the template context.
305

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

    
350
@login_required
351
@signed_terms_required
352
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
353
    """
354
    Allows a user to send feedback.
355

356
    In case of GET request renders a form for providing the feedback information.
357
    In case of POST sends an email to support team.
358

359
    If the user isn't logged in, redirects to settings.LOGIN_URL.
360

361
    **Arguments**
362

363
    ``template_name``
364
        A custom template to use. This is optional; if not specified,
365
        this will default to ``im/feedback.html``.
366

367
    ``extra_context``
368
        An dictionary of variables to add to the template context.
369

370
    **Template:**
371

372
    im/signup.html or ``template_name`` keyword argument.
373

374
    **Settings:**
375

376
    * LOGIN_URL: login uri
377
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
378
    """
379
    if request.method == 'GET':
380
        form = FeedbackForm()
381
    if request.method == 'POST':
382
        if not request.user:
383
            return HttpResponse('Unauthorized', status=401)
384

    
385
        form = FeedbackForm(request.POST)
386
        if form.is_valid():
387
            msg = form.cleaned_data['feedback_msg'],
388
            data = form.cleaned_data['feedback_data']
389
            try:
390
                send_feedback(msg, data, request.user, email_template_name)
391
            except SendMailError, e:
392
                message = e.message
393
                status = messages.ERROR
394
            else:
395
                message = _('Feedback successfully sent')
396
                status = messages.SUCCESS
397
            messages.add_message(request, status, message)
398
    return render_response(template_name,
399
                           feedback_form = form,
400
                           context_instance = get_context(request, extra_context))
401

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

    
423
@transaction.commit_manually
424
def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
425
    """
426
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
427
    and renews the user token.
428

429
    The view uses commit_manually decorator in order to ensure the user state will be updated
430
    only if the email will be send successfully.
431
    """
432
    token = request.GET.get('auth')
433
    next = request.GET.get('next')
434
    try:
435
        user = AstakosUser.objects.get(auth_token=token)
436
    except AstakosUser.DoesNotExist:
437
        return HttpResponseBadRequest(_('No such user'))
438
    
439
    try:
440
        local_user = AstakosUser.objects.get(email=user.email, is_active=True)
441
    except AstakosUser.DoesNotExist:
442
        user.is_active = True
443
        user.email_verified = True
444
        try:
445
            user.save()
446
        except ValidationError, e:
447
            return HttpResponseBadRequest(e)
448
        
449
    else:
450
        # switch the local account to shibboleth one
451
        local_user.provider = 'shibboleth'
452
        local_user.set_unusable_password()
453
        local_user.third_party_identifier = user.third_party_identifier
454
        try:
455
            local_user.save()
456
        except ValidationError, e:
457
            return HttpResponseBadRequest(e)
458
        user.delete()
459
        user = local_user
460
    
461
    try:
462
        send_greeting(user, email_template_name)
463
        response = prepare_response(request, user, next, renew=True)
464
        transaction.commit()
465
        return response
466
    except SendMailError, e:
467
        message = e.message
468
        messages.add_message(request, messages.ERROR, message)
469
        transaction.rollback()
470
        return signup(request, on_failure='im/signup.html')
471
    except BaseException, e:
472
        status = messages.ERROR
473
        message = _('Something went wrong.')
474
        logger.exception(e)
475
        transaction.rollback()
476
        return signup(request, on_failure='im/signup.html')
477

    
478
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
479
    term = None
480
    terms = None
481
    if not term_id:
482
        try:
483
            term = ApprovalTerms.objects.order_by('-id')[0]
484
        except IndexError:
485
            pass
486
    else:
487
        try:
488
             term = ApprovalTerms.objects.get(id=term_id)
489
        except ApprovalTermDoesNotExist, e:
490
            pass
491

    
492
    if not term:
493
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
494
    f = open(term.location, 'r')
495
    terms = f.read()
496

    
497
    if request.method == 'POST':
498
        next = request.POST.get('next')
499
        if not next:
500
            next = reverse('astakos.im.views.index')
501
        form = SignApprovalTermsForm(request.POST, instance=request.user)
502
        if not form.is_valid():
503
            return render_response(template_name,
504
                           terms = terms,
505
                           approval_terms_form = form,
506
                           context_instance = get_context(request, extra_context))
507
        user = form.save()
508
        return HttpResponseRedirect(next)
509
    else:
510
        form = None
511
        if request.user.is_authenticated() and not request.user.signed_terms():
512
            form = SignApprovalTermsForm(instance=request.user)
513
        return render_response(template_name,
514
                               terms = terms,
515
                               approval_terms_form = form,
516
                               context_instance = get_context(request, extra_context))
517

    
518
@signed_terms_required
519
def change_password(request):
520
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))