Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 678b2236

History | View | Annotate | Download (22.6 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.views.decorators.http import require_http_methods
56

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

    
68
logger = logging.getLogger(__name__)
69

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

    
85

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

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

    
115
@require_http_methods(["GET", "POST"])
116
@signed_terms_required
117
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
118
    """
119
    If there is logged on user renders the profile page otherwise renders login page.
120

121
    **Arguments**
122

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

127
    ``profile_template_name``
128
        A custom profile template to use. This is optional; if not specified,
129
        this will default to ``im/profile.html``.
130

131
    ``extra_context``
132
        An dictionary of variables to add to the template context.
133

134
    **Template:**
135

136
    im/profile.html or im/login.html or ``template_name`` keyword argument.
137

138
    """
139
    template_name = login_template_name
140
    if request.user.is_authenticated():
141
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
142
    
143
    return render_response(
144
        template_name,
145
        login_form = LoginForm(request=request),
146
        context_instance = get_context(request, extra_context)
147
    )
148

    
149
@require_http_methods(["GET", "POST"])
150
@login_required
151
@signed_terms_required
152
@transaction.commit_manually
153
def invite(request, template_name='im/invitations.html', extra_context={}):
154
    """
155
    Allows a user to invite somebody else.
156

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

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

164
    If the user isn't logged in, redirects to settings.LOGIN_URL.
165

166
    **Arguments**
167

168
    ``template_name``
169
        A custom template to use. This is optional; if not specified,
170
        this will default to ``im/invitations.html``.
171

172
    ``extra_context``
173
        An dictionary of variables to add to the template context.
174

175
    **Template:**
176

177
    im/invitations.html or ``template_name`` keyword argument.
178

179
    **Settings:**
180

181
    The view expectes the following settings are defined:
182

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

    
217
    sent = [{'email': inv.username,
218
             'realname': inv.realname,
219
             'is_consumed': inv.is_consumed}
220
             for inv in request.user.invitations_sent.all()]
221
    kwargs = {'inviter': inviter,
222
              'sent':sent}
223
    context = get_context(request, extra_context, **kwargs)
224
    return render_response(template_name,
225
                           invitation_form = form,
226
                           context_instance = context)
227

    
228
@require_http_methods(["GET", "POST"])
229
@login_required
230
@signed_terms_required
231
def edit_profile(request, template_name='im/profile.html', extra_context={}):
232
    """
233
    Allows a user to edit his/her profile.
234

235
    In case of GET request renders a form for displaying the user information.
236
    In case of POST updates the user informantion and redirects to ``next``
237
    url parameter if exists.
238

239
    If the user isn't logged in, redirects to settings.LOGIN_URL.
240

241
    **Arguments**
242

243
    ``template_name``
244
        A custom template to use. This is optional; if not specified,
245
        this will default to ``im/profile.html``.
246

247
    ``extra_context``
248
        An dictionary of variables to add to the template context.
249

250
    **Template:**
251

252
    im/profile.html or ``template_name`` keyword argument.
253

254
    **Settings:**
255

256
    The view expectes the following settings are defined:
257

258
    * LOGIN_URL: login uri
259
    """
260
    form = ProfileForm(instance=request.user)
261
    extra_context['next'] = request.GET.get('next')
262
    reset_cookie = False
263
    if request.method == 'POST':
264
        form = ProfileForm(request.POST, instance=request.user)
265
        if form.is_valid():
266
            try:
267
                prev_token = request.user.auth_token
268
                user = form.save()
269
                reset_cookie = user.auth_token != prev_token
270
                form = ProfileForm(instance=user)
271
                next = request.POST.get('next')
272
                if next:
273
                    return redirect(next)
274
                msg = _('<p>Profile has been updated successfully</p>')
275
                messages.add_message(request, messages.SUCCESS, msg)
276
            except ValueError, ve:
277
                messages.add_message(request, messages.ERROR, ve)
278
    elif request.method == "GET":
279
        request.user.is_verified = True
280
        request.user.save()
281
    return render_response(template_name,
282
                           reset_cookie = reset_cookie,
283
                           profile_form = form,
284
                           context_instance = get_context(request,
285
                                                          extra_context))
286

    
287
@require_http_methods(["GET", "POST"])
288
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
289
    """
290
    Allows a user to create a local account.
291

292
    In case of GET request renders a form for entering the user information.
293
    In case of POST handles the signup.
294

295
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
296
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
297
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
298
    (see activation_backends);
299
    
300
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
301
    otherwise renders the same page with a success message.
302
    
303
    On unsuccessful creation, renders ``template_name`` with an error message.
304
    
305
    **Arguments**
306
    
307
    ``template_name``
308
        A custom template to render. This is optional;
309
        if not specified, this will default to ``im/signup.html``.
310

311
    ``on_success``
312
        A custom template to render in case of success. This is optional;
313
        if not specified, this will default to ``im/signup_complete.html``.
314

315
    ``extra_context``
316
        An dictionary of variables to add to the template context.
317

318
    **Template:**
319
    
320
    im/signup.html or ``template_name`` keyword argument.
321
    im/signup_complete.html or ``on_success`` keyword argument. 
322
    """
323
    if request.user.is_authenticated():
324
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
325
    
326
    provider = get_query(request).get('provider', 'local')
327
    try:
328
        if not backend:
329
            backend = get_backend(request)
330
        form = backend.get_signup_form(provider)
331
    except Exception, e:
332
        form = SimpleBackend(request).get_signup_form(provider)
333
        messages.add_message(request, messages.ERROR, e)
334
    if request.method == 'POST':
335
        if form.is_valid():
336
            user = form.save(commit=False)
337
            try:
338
                result = backend.handle_activation(user)
339
                status = messages.SUCCESS
340
                message = result.message
341
                user.save()
342
                if 'additional_email' in form.cleaned_data:
343
                    additional_email = form.cleaned_data['additional_email']
344
                    if additional_email != user.email:
345
                        user.additionalmail_set.create(email=additional_email)
346
                        msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
347
                        logger._log(LOGGING_LEVEL, msg, [])
348
                if user and user.is_active:
349
                    next = request.POST.get('next', '')
350
                    return prepare_response(request, user, next=next)
351
                messages.add_message(request, status, message)
352
                return render_response(on_success,
353
                                       context_instance=get_context(request, extra_context))
354
            except SendMailError, e:
355
                status = messages.ERROR
356
                message = e.message
357
                messages.add_message(request, status, message)
358
            except BaseException, e:
359
                logger.exception(e)
360
                status = messages.ERROR
361
                message = _('Something went wrong.')
362
                messages.add_message(request, status, message)
363
                logger.exception(e)
364
    return render_response(template_name,
365
                           signup_form = form,
366
                           provider = provider,
367
                           context_instance=get_context(request, extra_context))
368

    
369
@require_http_methods(["GET", "POST"])
370
@login_required
371
@signed_terms_required
372
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
373
    """
374
    Allows a user to send feedback.
375

376
    In case of GET request renders a form for providing the feedback information.
377
    In case of POST sends an email to support team.
378

379
    If the user isn't logged in, redirects to settings.LOGIN_URL.
380

381
    **Arguments**
382

383
    ``template_name``
384
        A custom template to use. This is optional; if not specified,
385
        this will default to ``im/feedback.html``.
386

387
    ``extra_context``
388
        An dictionary of variables to add to the template context.
389

390
    **Template:**
391

392
    im/signup.html or ``template_name`` keyword argument.
393

394
    **Settings:**
395

396
    * LOGIN_URL: login uri
397
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
398
    """
399
    if request.method == 'GET':
400
        form = FeedbackForm()
401
    if request.method == 'POST':
402
        if not request.user:
403
            return HttpResponse('Unauthorized', status=401)
404

    
405
        form = FeedbackForm(request.POST)
406
        if form.is_valid():
407
            msg = form.cleaned_data['feedback_msg']
408
            data = form.cleaned_data['feedback_data']
409
            try:
410
                send_feedback(msg, data, request.user, email_template_name)
411
            except SendMailError, e:
412
                message = e.message
413
                status = messages.ERROR
414
            else:
415
                message = _('Feedback successfully sent')
416
                status = messages.SUCCESS
417
            messages.add_message(request, status, message)
418
    return render_response(template_name,
419
                           feedback_form = form,
420
                           context_instance = get_context(request, extra_context))
421

    
422
@require_http_methods(["GET", "POST"])
423
def logout(request, template='registration/logged_out.html', extra_context={}):
424
    """
425
    Wraps `django.contrib.auth.logout` and delete the cookie.
426
    """
427
    response = HttpResponse()
428
    if request.user.is_authenticated():
429
        email = request.user.email
430
        auth_logout(request)
431
        response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
432
        msg = 'Cookie deleted for %s' % email
433
        logger._log(LOGGING_LEVEL, msg, [])
434
    next = request.GET.get('next')
435
    if next:
436
        response['Location'] = next
437
        response.status_code = 302
438
        return response
439
    elif LOGOUT_NEXT:
440
        response['Location'] = LOGOUT_NEXT
441
        response.status_code = 301
442
        return response
443
    messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
444
    context = get_context(request, extra_context)
445
    response.write(render_to_string(template, context_instance=context))
446
    return response
447

    
448
@require_http_methods(["GET", "POST"])
449
@transaction.commit_manually
450
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
451
    """
452
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
453
    and renews the user token.
454

455
    The view uses commit_manually decorator in order to ensure the user state will be updated
456
    only if the email will be send successfully.
457
    """
458
    token = request.GET.get('auth')
459
    next = request.GET.get('next')
460
    try:
461
        user = AstakosUser.objects.get(auth_token=token)
462
    except AstakosUser.DoesNotExist:
463
        return HttpResponseBadRequest(_('No such user'))
464
    
465
    if user.is_active:
466
        message = _('Account already active.')
467
        messages.add_message(request, messages.ERROR, message)
468
        return index(request)
469
    
470
    try:
471
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
472
        response = prepare_response(request, user, next, renew=True)
473
        transaction.commit()
474
        return response
475
    except SendMailError, e:
476
        message = e.message
477
        messages.add_message(request, messages.ERROR, message)
478
        transaction.rollback()
479
        return index(request)
480
    except BaseException, e:
481
        status = messages.ERROR
482
        message = _('Something went wrong.')
483
        messages.add_message(request, messages.ERROR, message)
484
        logger.exception(e)
485
        transaction.rollback()
486
        return index(request)
487

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

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

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

    
529
@require_http_methods(["GET", "POST"])
530
@signed_terms_required
531
def change_password(request):
532
    return password_change(request,
533
                            post_change_redirect=reverse('astakos.im.views.edit_profile'),
534
                            password_change_form=ExtendedPasswordChangeForm)
535

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