Statistics
| Branch: | Tag: | Revision:

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

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

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

    
64
logger = logging.getLogger(__name__)
65

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

    
81

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

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

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

116
    **Arguments**
117

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

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

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

129
    **Template:**
130

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

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

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

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

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

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

157
    **Arguments**
158

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

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

166
    **Template:**
167

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

170
    **Settings:**
171

172
    The view expectes the following settings are defined:
173

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

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

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

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

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

231
    **Arguments**
232

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

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

240
    **Template:**
241

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

244
    **Settings:**
245

246
    The view expectes the following settings are defined:
247

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

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

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

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

297

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

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

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

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

359
    In case of GET request renders a form for providing the feedback information.
360
    In case of POST sends an email to support team.
361

362
    If the user isn't logged in, redirects to settings.LOGIN_URL.
363

364
    **Arguments**
365

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

370
    ``extra_context``
371
        An dictionary of variables to add to the template context.
372

373
    **Template:**
374

375
    im/signup.html or ``template_name`` keyword argument.
376

377
    **Settings:**
378

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

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

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

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

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

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

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

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

    
521
@signed_terms_required
522
def change_password(request):
523
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
524

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