Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.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 (
43
    HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
44
)
45
from django.shortcuts import redirect
46
from django.template.loader import render_to_string
47
from django.utils.translation import ugettext as _
48
from django.core.urlresolvers import reverse
49
from django.contrib.auth.decorators import login_required
50
from django.contrib import messages
51
from django.db import transaction
52
from django.utils.http import urlencode
53
from django.db.utils import IntegrityError
54
from django.contrib.auth.views import password_change
55
from django.core.exceptions import ValidationError
56
from django.views.decorators.http import require_http_methods
57

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

    
73
logger = logging.getLogger(__name__)
74

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

    
88

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

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

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

124
    **Arguments**
125

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

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

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

137
    **Template:**
138

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

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

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

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

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

168
    If the user isn't logged in, redirects to settings.LOGIN_URL.
169

170
    **Arguments**
171

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

176
    ``extra_context``
177
        An dictionary of variables to add to the template context.
178

179
    **Template:**
180

181
    im/invitations.html or ``template_name`` keyword argument.
182

183
    **Settings:**
184

185
    The view expectes the following settings are defined:
186

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

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

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

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

244
    If the user isn't logged in, redirects to settings.LOGIN_URL.
245

246
    **Arguments**
247

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

252
    ``extra_context``
253
        An dictionary of variables to add to the template context.
254

255
    **Template:**
256

257
    im/profile.html or ``template_name`` keyword argument.
258

259
    **Settings:**
260

261
    The view expectes the following settings are defined:
262

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

    
303
@require_http_methods(["GET", "POST"])
304
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
305
    """
306
    Allows a user to create a local account.
307

308
    In case of GET request renders a form for entering the user information.
309
    In case of POST handles the signup.
310

311
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
312
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
313
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
314
    (see activation_backends);
315
    
316
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
317
    otherwise renders the same page with a success message.
318
    
319
    On unsuccessful creation, renders ``template_name`` with an error message.
320
    
321
    **Arguments**
322
    
323
    ``template_name``
324
        A custom template to render. This is optional;
325
        if not specified, this will default to ``im/signup.html``.
326

327
    ``on_success``
328
        A custom template to render in case of success. This is optional;
329
        if not specified, this will default to ``im/signup_complete.html``.
330

331
    ``extra_context``
332
        An dictionary of variables to add to the template context.
333

334
    **Template:**
335
    
336
    im/signup.html or ``template_name`` keyword argument.
337
    im/signup_complete.html or ``on_success`` keyword argument. 
338
    """
339
    extra_context = extra_context or {}
340
    if request.user.is_authenticated():
341
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
342
    
343
    provider = get_query(request).get('provider', 'local')
344
    id = get_query(request).get('id')
345
    try:
346
        instance = AstakosUser.objects.get(id=id) if id else None
347
    except AstakosUser.DoesNotExist:
348
        instance = None
349

    
350
    try:
351
        if not backend:
352
            backend = get_backend(request)
353
        form = backend.get_signup_form(provider, instance)
354
    except Exception, e:
355
        form = SimpleBackend(request).get_signup_form(provider)
356
        messages.add_message(request, messages.ERROR, e)
357
    if request.method == 'POST':
358
        if form.is_valid():
359
            user = form.save(commit=False)
360
            try:
361
                result = backend.handle_activation(user)
362
                status = messages.SUCCESS
363
                message = result.message
364
                user.save()
365
                if 'additional_email' in form.cleaned_data:
366
                    additional_email = form.cleaned_data['additional_email']
367
                    if additional_email != user.email:
368
                        user.additionalmail_set.create(email=additional_email)
369
                        msg = 'Additional email: %s saved for user %s.' % (
370
                            additional_email,
371
                            user.email
372
                        )
373
                        logger._log(LOGGING_LEVEL, msg, [])
374
                if user and user.is_active:
375
                    next = request.POST.get('next', '')
376
                    return prepare_response(request, user, next=next)
377
                messages.add_message(request, status, message)
378
                return render_response(
379
                    on_success,
380
                    context_instance=get_context(
381
                        request,
382
                        extra_context
383
                    )
384
                )
385
            except SendMailError, e:
386
                logger.exception(e)
387
                status = messages.ERROR
388
                message = e.message
389
                messages.add_message(request, status, message)
390
            except BaseException, e:
391
                logger.exception(e)
392
                status = messages.ERROR
393
                message = _('Something went wrong.')
394
                messages.add_message(request, status, message)
395
                logger.exception(e)
396
    return render_response(template_name,
397
                           signup_form = form,
398
                           provider = provider,
399
                           context_instance=get_context(request, extra_context))
400

    
401
@require_http_methods(["GET", "POST"])
402
@login_required
403
@signed_terms_required
404
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
405
    """
406
    Allows a user to send feedback.
407

408
    In case of GET request renders a form for providing the feedback information.
409
    In case of POST sends an email to support team.
410

411
    If the user isn't logged in, redirects to settings.LOGIN_URL.
412

413
    **Arguments**
414

415
    ``template_name``
416
        A custom template to use. This is optional; if not specified,
417
        this will default to ``im/feedback.html``.
418

419
    ``extra_context``
420
        An dictionary of variables to add to the template context.
421

422
    **Template:**
423

424
    im/signup.html or ``template_name`` keyword argument.
425

426
    **Settings:**
427

428
    * LOGIN_URL: login uri
429
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
430
    """
431
    extra_context = extra_context or {}
432
    if request.method == 'GET':
433
        form = FeedbackForm()
434
    if request.method == 'POST':
435
        if not request.user:
436
            return HttpResponse('Unauthorized', status=401)
437

    
438
        form = FeedbackForm(request.POST)
439
        if form.is_valid():
440
            msg = form.cleaned_data['feedback_msg']
441
            data = form.cleaned_data['feedback_data']
442
            try:
443
                send_feedback(msg, data, request.user, email_template_name)
444
            except SendMailError, e:
445
                message = e.message
446
                status = messages.ERROR
447
            else:
448
                message = _('Feedback successfully sent')
449
                status = messages.SUCCESS
450
            messages.add_message(request, status, message)
451
    return render_response(template_name,
452
                           feedback_form = form,
453
                           context_instance = get_context(request, extra_context))
454

    
455
@require_http_methods(["GET"])
456
def logout(request, template='registration/logged_out.html', extra_context=None):
457
    """
458
    Wraps `django.contrib.auth.logout`.
459
    """
460
    extra_context = extra_context or {}
461
    response = HttpResponse()
462
    if request.user.is_authenticated():
463
        email = request.user.email
464
        auth_logout(request)
465
    next = restrict_next(
466
        request.GET.get('next'),
467
        domain=COOKIE_DOMAIN
468
    )
469
    if next:
470
        response['Location'] = next
471
        response.status_code = 302
472
    elif LOGOUT_NEXT:
473
        response['Location'] = LOGOUT_NEXT
474
        response.status_code = 301
475
    else:
476
        messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
477
        context = get_context(request, extra_context)
478
        response.write(render_to_string(template, context_instance=context))
479
    return response
480

    
481
@require_http_methods(["GET", "POST"])
482
@transaction.commit_manually
483
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
484
    """
485
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
486
    and renews the user token.
487

488
    The view uses commit_manually decorator in order to ensure the user state will be updated
489
    only if the email will be send successfully.
490
    """
491
    token = request.GET.get('auth')
492
    next = request.GET.get('next')
493
    try:
494
        user = AstakosUser.objects.get(auth_token=token)
495
    except AstakosUser.DoesNotExist:
496
        return HttpResponseBadRequest(_('No such user'))
497
    
498
    if user.is_active:
499
        message = _('Account already active.')
500
        messages.add_message(request, messages.ERROR, message)
501
        return index(request)
502
    
503
    try:
504
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
505
        response = prepare_response(request, user, next, renew=True)
506
        transaction.commit()
507
        return response
508
    except SendMailError, e:
509
        message = e.message
510
        messages.add_message(request, messages.ERROR, message)
511
        transaction.rollback()
512
        return index(request)
513
    except BaseException, e:
514
        status = messages.ERROR
515
        message = _('Something went wrong.')
516
        messages.add_message(request, messages.ERROR, message)
517
        logger.exception(e)
518
        transaction.rollback()
519
        return index(request)
520

    
521
@require_http_methods(["GET", "POST"])
522
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
523
    extra_context = extra_context or {}
524
    term = None
525
    terms = None
526
    if not term_id:
527
        try:
528
            term = ApprovalTerms.objects.order_by('-id')[0]
529
        except IndexError:
530
            pass
531
    else:
532
        try:
533
             term = ApprovalTerms.objects.get(id=term_id)
534
        except ApprovalTermDoesNotExist, e:
535
            pass
536

    
537
    if not term:
538
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
539
    f = open(term.location, 'r')
540
    terms = f.read()
541

    
542
    if request.method == 'POST':
543
        next = restrict_next(
544
            request.POST.get('next'),
545
            domain=COOKIE_DOMAIN
546
        )
547
        if not next:
548
            next = reverse('astakos.im.views.index')
549
        form = SignApprovalTermsForm(request.POST, instance=request.user)
550
        if not form.is_valid():
551
            return render_response(template_name,
552
                           terms = terms,
553
                           approval_terms_form = form,
554
                           context_instance = get_context(request, extra_context))
555
        user = form.save()
556
        return HttpResponseRedirect(next)
557
    else:
558
        form = None
559
        if request.user.is_authenticated() and not request.user.signed_terms():
560
            form = SignApprovalTermsForm(instance=request.user)
561
        return render_response(template_name,
562
                               terms = terms,
563
                               approval_terms_form = form,
564
                               context_instance = get_context(request, extra_context))
565

    
566
@require_http_methods(["GET", "POST"])
567
@login_required
568
@signed_terms_required
569
@transaction.commit_manually
570
def change_email(request, activation_key=None,
571
                 email_template_name='registration/email_change_email.txt',
572
                 form_template_name='registration/email_change_form.html',
573
                 confirm_template_name='registration/email_change_done.html',
574
                 extra_context=None):
575
    extra_context = extra_context or {}
576
    if activation_key:
577
        try:
578
            user = EmailChange.objects.change_email(activation_key)
579
            if request.user.is_authenticated() and request.user == user:
580
                msg = _('Email changed successfully.')
581
                messages.add_message(request, messages.SUCCESS, msg)
582
                auth_logout(request)
583
                response = prepare_response(request, user)
584
                transaction.commit()
585
                return response
586
        except ValueError, e:
587
            messages.add_message(request, messages.ERROR, e)
588
        return render_response(confirm_template_name,
589
                               modified_user = user if 'user' in locals() else None,
590
                               context_instance = get_context(request,
591
                                                              extra_context))
592
    
593
    if not request.user.is_authenticated():
594
        path = quote(request.get_full_path())
595
        url = request.build_absolute_uri(reverse('astakos.im.views.index'))
596
        return HttpResponseRedirect(url + '?next=' + path)
597
    form = EmailChangeForm(request.POST or None)
598
    if request.method == 'POST' and form.is_valid():
599
        try:
600
            ec = form.save(email_template_name, request)
601
        except SendMailError, e:
602
            status = messages.ERROR
603
            msg = e
604
            transaction.rollback()
605
        except IntegrityError, e:
606
            status = messages.ERROR
607
            msg = _('There is already a pending change email request.')
608
        else:
609
            status = messages.SUCCESS
610
            msg = _('Change email request has been registered succefully.\
611
                    You are going to receive a verification email in the new address.')
612
            transaction.commit()
613
        messages.add_message(request, status, msg)
614
    return render_response(form_template_name,
615
                           form = form,)
616

    
617

    
618
def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
619
    extra_context = extra_context or {}
620
    try:
621
        u = AstakosUser.objects.get(id=user_id)
622
    except AstakosUser.DoesNotExist:
623
        messages.error(request, _('Invalid user id'))
624
    else:
625
        try:
626
            send_activation_func(u)
627
            msg = _('Activation sent.')
628
            messages.success(request, msg)
629
        except SendMailError, e:
630
            messages.error(request, e)
631
    return render_response(
632
        template_name,
633
        login_form = LoginForm(request=request), 
634
        context_instance = get_context(
635
            request,
636
            extra_context
637
        )
638
    )
639