Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 217994f8

History | View | Annotate | Download (22.9 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import socket
36

    
37
from smtplib import SMTPException
38
from urllib import quote
39
from functools import wraps
40

    
41
from django.core.mail import send_mail
42
from django.http import HttpResponse, HttpResponseBadRequest
43
from django.shortcuts import redirect
44
from django.template.loader import render_to_string
45
from django.utils.translation import ugettext as _
46
from django.core.urlresolvers import reverse
47
from django.contrib.auth.decorators import login_required
48
from django.contrib import messages
49
from django.db import transaction
50
from django.utils.http import urlencode
51
from django.http import HttpResponseRedirect, HttpResponseBadRequest
52
from django.db.utils import IntegrityError
53
from django.contrib.auth.views import password_change
54
from django.core.exceptions import ValidationError
55
from django.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 (
60
    get_context, prepare_response, set_cookie, get_query, restrict_next
61
)
62
from astakos.im.forms import *
63
from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
64
    invite as invite_func, logout as auth_logout, activate as activate_func
65
)
66
from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL,
67
    COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
68
)
69

    
70
logger = logging.getLogger(__name__)
71

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

    
87

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

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

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

123
    **Arguments**
124

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

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

133
    ``extra_context``
134
        An dictionary of variables to add to the template context.
135

136
    **Template:**
137

138
    im/profile.html or im/login.html or ``template_name`` keyword argument.
139

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

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

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

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

166
    If the user isn't logged in, redirects to settings.LOGIN_URL.
167

168
    **Arguments**
169

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

174
    ``extra_context``
175
        An dictionary of variables to add to the template context.
176

177
    **Template:**
178

179
    im/invitations.html or ``template_name`` keyword argument.
180

181
    **Settings:**
182

183
    The view expectes the following settings are defined:
184

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

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

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

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

241
    If the user isn't logged in, redirects to settings.LOGIN_URL.
242

243
    **Arguments**
244

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

249
    ``extra_context``
250
        An dictionary of variables to add to the template context.
251

252
    **Template:**
253

254
    im/profile.html or ``template_name`` keyword argument.
255

256
    **Settings:**
257

258
    The view expectes the following settings are defined:
259

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

    
292
@require_http_methods(["GET", "POST"])
293
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
294
    """
295
    Allows a user to create a local account.
296

297
    In case of GET request renders a form for entering the user information.
298
    In case of POST handles the signup.
299

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

316
    ``on_success``
317
        A custom template to render in case of success. This is optional;
318
        if not specified, this will default to ``im/signup_complete.html``.
319

320
    ``extra_context``
321
        An dictionary of variables to add to the template context.
322

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

    
374
@require_http_methods(["GET", "POST"])
375
@login_required
376
@signed_terms_required
377
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
378
    """
379
    Allows a user to send feedback.
380

381
    In case of GET request renders a form for providing the feedback information.
382
    In case of POST sends an email to support team.
383

384
    If the user isn't logged in, redirects to settings.LOGIN_URL.
385

386
    **Arguments**
387

388
    ``template_name``
389
        A custom template to use. This is optional; if not specified,
390
        this will default to ``im/feedback.html``.
391

392
    ``extra_context``
393
        An dictionary of variables to add to the template context.
394

395
    **Template:**
396

397
    im/signup.html or ``template_name`` keyword argument.
398

399
    **Settings:**
400

401
    * LOGIN_URL: login uri
402
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
403
    """
404
    if request.method == 'GET':
405
        form = FeedbackForm()
406
    if request.method == 'POST':
407
        if not request.user:
408
            return HttpResponse('Unauthorized', status=401)
409

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

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

    
456
@require_http_methods(["GET", "POST"])
457
@transaction.commit_manually
458
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
459
    """
460
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
461
    and renews the user token.
462

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

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

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

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

    
540
@require_http_methods(["GET", "POST"])
541
@signed_terms_required
542
def change_password(request):
543
    return password_change(request,
544
                            post_change_redirect=reverse('astakos.im.views.edit_profile'),
545
                            password_change_form=ExtendedPasswordChangeForm)
546

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