Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 4e30244e

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

    
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, has_signed_terms
59
from astakos.im.forms import *
60
from astakos.im.functions import send_greeting, send_feedback, SendMailError
61
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT
62
from astakos.im.functions import invite as invite_func
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 has_signed_terms(request.user):
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(),
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
        
186
        if inviter.invitations > 0:
187
            if form.is_valid():
188
                try:
189
                    invitation = form.save()
190
                    invite_func(invitation, inviter)
191
                    status = messages.SUCCESS
192
                    message = _('Invitation sent to %s' % invitation.username)
193
                except SendMailError, e:
194
                    status = messages.ERROR
195
                    message = e.message
196
                    transaction.rollback()
197
                except BaseException, e:
198
                    status = messages.ERROR
199
                    message = _('Something went wrong.')
200
                    logger.exception(e)
201
                    transaction.rollback()
202
                else:
203
                    transaction.commit()
204
        else:
205
            status = messages.ERROR
206
            message = _('No invitations left')
207
    messages.add_message(request, status, message)
208
    
209
    sent = [{'email': inv.username,
210
             'realname': inv.realname,
211
             'is_consumed': inv.is_consumed}
212
             for inv in request.user.invitations_sent.all()]
213
    kwargs = {'inviter': inviter,
214
              'sent':sent}
215
    context = get_context(request, extra_context, **kwargs)
216
    return render_response(template_name,
217
                           invitation_form = form,
218
                           context_instance = context)
219

    
220
@login_required
221
@signed_terms_required
222
def edit_profile(request, template_name='im/profile.html', extra_context={}):
223
    """
224
    Allows a user to edit his/her profile.
225
    
226
    In case of GET request renders a form for displaying the user information.
227
    In case of POST updates the user informantion and redirects to ``next``
228
    url parameter if exists.
229
    
230
    If the user isn't logged in, redirects to settings.LOGIN_URL.
231
    
232
    **Arguments**
233
    
234
    ``template_name``
235
        A custom template to use. This is optional; if not specified,
236
        this will default to ``im/profile.html``.
237
    
238
    ``extra_context``
239
        An dictionary of variables to add to the template context.
240
    
241
    **Template:**
242
    
243
    im/profile.html or ``template_name`` keyword argument.
244
    
245
    **Settings:**
246
    
247
    The view expectes the following settings are defined:
248
    
249
    * LOGIN_URL: login uri
250
    """
251
    form = ProfileForm(instance=request.user)
252
    extra_context['next'] = request.GET.get('next')
253
    reset_cookie = False
254
    if request.method == 'POST':
255
        form = ProfileForm(request.POST, instance=request.user)
256
        if form.is_valid():
257
            try:
258
                prev_token = request.user.auth_token
259
                user = form.save()
260
                reset_cookie = user.auth_token != prev_token
261
                form = ProfileForm(instance=user)
262
                next = request.POST.get('next')
263
                if next:
264
                    return redirect(next)
265
                msg = _('Profile has been updated successfully')
266
                messages.add_message(request, messages.SUCCESS, msg)
267
            except ValueError, ve:
268
                messages.add_message(request, messages.ERROR, ve)
269
    return render_response(template_name,
270
                           reset_cookie = reset_cookie,
271
                           profile_form = form,
272
                           context_instance = get_context(request,
273
                                                          extra_context))
274

    
275
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
276
    """
277
    Allows a user to create a local account.
278
    
279
    In case of GET request renders a form for providing the user information.
280
    In case of POST handles the signup.
281
    
282
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
283
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
284
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
285
    (see activation_backends);
286
    
287
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
288
    otherwise renders the same page with a success message.
289
    
290
    On unsuccessful creation, renders ``template_name`` with an error message.
291
    
292
    **Arguments**
293
    
294
    ``template_name``
295
        A custom template to render. This is optional;
296
        if not specified, this will default to ``im/signup.html``.
297
    
298
    
299
    ``on_success``
300
        A custom template to render in case of success. This is optional;
301
        if not specified, this will default to ``im/signup_complete.html``.
302
    
303
    ``extra_context``
304
        An dictionary of variables to add to the template context.
305
    
306
    **Template:**
307
    
308
    im/signup.html or ``template_name`` keyword argument.
309
    im/signup_complete.html or ``on_success`` keyword argument. 
310
    """
311
    if request.user.is_authenticated():
312
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
313
    
314
    query_dict = request.__getattribute__(request.method)
315
    provider = query_dict.get('provider', 'local')
316
    try:
317
        if not backend:
318
            backend = get_backend(request)
319
        form = backend.get_signup_form(provider)
320
    except (Invitation.DoesNotExist, ValueError), e:
321
        form = SimpleBackend(request).get_signup_form(provider)
322
        messages.add_message(request, messages.ERROR, e)
323
    if request.method == 'POST':
324
        if form.is_valid():
325
            user = form.save(commit=False)
326
            try:
327
                result = backend.handle_activation(user)
328
                status = messages.SUCCESS
329
                message = result.message
330
                user.save()
331
                if user and user.is_active:
332
                    next = request.POST.get('next', '')
333
                    return prepare_response(request, user, next=next)
334
                messages.add_message(request, status, message)
335
                return render_response(on_success,
336
                                       context_instance=get_context(request, extra_context))
337
            except SendMailError, e:
338
                status = messages.ERROR
339
                message = e.message
340
                messages.add_message(request, status, message)
341
            except BaseException, e:
342
                status = messages.ERROR
343
                message = _('Something went wrong.')
344
                messages.add_message(request, status, message)
345
    return render_response(template_name,
346
                           signup_form = form,
347
                           context_instance=get_context(request, extra_context))
348

    
349
@login_required
350
@signed_terms_required
351
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
352
    """
353
    Allows a user to send feedback.
354
    
355
    In case of GET request renders a form for providing the feedback information.
356
    In case of POST sends an email to support team.
357
    
358
    If the user isn't logged in, redirects to settings.LOGIN_URL.
359
    
360
    **Arguments**
361
    
362
    ``template_name``
363
        A custom template to use. This is optional; if not specified,
364
        this will default to ``im/feedback.html``.
365
    
366
    ``extra_context``
367
        An dictionary of variables to add to the template context.
368
    
369
    **Template:**
370
    
371
    im/signup.html or ``template_name`` keyword argument.
372
    
373
    **Settings:**
374
    
375
    * LOGIN_URL: login uri
376
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
377
    """
378
    if request.method == 'GET':
379
        form = FeedbackForm()
380
    if request.method == 'POST':
381
        if not request.user:
382
            return HttpResponse('Unauthorized', status=401)
383
        
384
        form = FeedbackForm(request.POST)
385
        if form.is_valid():
386
            msg = form.cleaned_data['feedback_msg'],
387
            data = form.cleaned_data['feedback_data']
388
            try:
389
                send_feedback(msg, data, request.user, email_template_name)
390
            except SendMailError, e:
391
                message = e.message
392
                status = messages.ERROR
393
            else:
394
                message = _('Feedback successfully sent')
395
                status = messages.SUCCESS
396
            messages.add_message(request, status, message)
397
    return render_response(template_name,
398
                           feedback_form = form,
399
                           context_instance = get_context(request, extra_context))
400

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

    
422
@transaction.commit_manually
423
def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
424
    """
425
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
426
    and renews the user token.
427
    
428
    The view uses commit_manually decorator in order to ensure the user state will be updated
429
    only if the email will be send successfully.
430
    """
431
    token = request.GET.get('auth')
432
    next = request.GET.get('next')
433
    try:
434
        user = AstakosUser.objects.get(auth_token=token)
435
    except AstakosUser.DoesNotExist:
436
        return HttpResponseBadRequest(_('No such user'))
437
    
438
    user.is_active = True
439
    user.email_verified = True
440
    user.save()
441
    try:
442
        send_greeting(user, email_template_name)
443
        response = prepare_response(request, user, next, renew=True)
444
        transaction.commit()
445
        return response
446
    except SendEmailError, e:
447
        message = e.message
448
        messages.add_message(request, messages.ERROR, message)
449
        transaction.rollback()
450
        return signup(request, on_failure='im/signup.html')
451
    except BaseException, e:
452
        status = messages.ERROR
453
        message = _('Something went wrong.')
454
        logger.exception(e)
455
        transaction.rollback()
456
        return signup(request, on_failure='im/signup.html')
457

    
458
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
459
    term = None
460
    terms = None
461
    if not term_id:
462
        try:
463
            term = ApprovalTerms.objects.order_by('-id')[0]
464
        except IndexError:
465
            pass
466
    else:
467
        try:
468
             term = ApprovalTerms.objects.get(id=term_id)
469
        except ApprovalTermDoesNotExist, e:
470
            pass
471
    
472
    if not term:
473
        return HttpResponseBadRequest(_('No approval terms found.'))
474
    f = open(term.location, 'r')
475
    terms = f.read()
476
    
477
    if request.method == 'POST':
478
        next = request.POST.get('next')
479
        if not next:
480
            next = reverse('astakos.im.views.index')
481
        form = SignApprovalTermsForm(request.POST, instance=request.user)
482
        if not form.is_valid():
483
            return render_response(template_name,
484
                           terms = terms,
485
                           approval_terms_form = form,
486
                           context_instance = get_context(request, extra_context))
487
        user = form.save()
488
        return HttpResponseRedirect(next)
489
    else:
490
        form = None
491
        if request.user.is_authenticated() and not has_signed_terms(request.user):
492
            form = SignApprovalTermsForm(instance=request.user)
493
        return render_response(template_name,
494
                               terms = terms,
495
                               approval_terms_form = form,
496
                               context_instance = get_context(request, extra_context))
497

    
498
@signed_terms_required
499
def change_password(request):
500
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))