Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 586967c0

History | View | Annotate | Download (19 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.backends import get_backend
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
61
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, 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
    formclass = 'LoginForm'
136
    kwargs = {}
137
    if request.user.is_authenticated():
138
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
139
    return render_response(template_name,
140
                           form = globals()[formclass](**kwargs),
141
                           context_instance = get_context(request, extra_context))
142

    
143
@login_required
144
@signed_terms_required
145
@transaction.commit_manually
146
def invite(request, template_name='im/invitations.html', extra_context={}):
147
    """
148
    Allows a user to invite somebody else.
149
    
150
    In case of GET request renders a form for providing the invitee information.
151
    In case of POST checks whether the user has not run out of invitations and then
152
    sends an invitation email to singup to the service.
153
    
154
    The view uses commit_manually decorator in order to ensure the number of the
155
    user invitations is going to be updated only if the email has been successfully sent.
156
    
157
    If the user isn't logged in, redirects to settings.LOGIN_URL.
158
    
159
    **Arguments**
160
    
161
    ``template_name``
162
        A custom template to use. This is optional; if not specified,
163
        this will default to ``im/invitations.html``.
164
    
165
    ``extra_context``
166
        An dictionary of variables to add to the template context.
167
    
168
    **Template:**
169
    
170
    im/invitations.html or ``template_name`` keyword argument.
171
    
172
    **Settings:**
173
    
174
    The view expectes the following settings are defined:
175
    
176
    * LOGIN_URL: login uri
177
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
178
    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
179
    """
180
    status = None
181
    message = None
182
    inviter = AstakosUser.objects.get(username = request.user.username)
183
    
184
    if request.method == 'POST':
185
        username = request.POST.get('uniq')
186
        realname = request.POST.get('realname')
187
        
188
        if inviter.invitations > 0:
189
            try:
190
                invite_func(inviter, username, realname)
191
                status = messages.SUCCESS
192
                message = _('Invitation sent to %s' % username)
193
                transaction.commit()
194
            except (SMTPException, socket.error) as e:
195
                status = messages.ERROR
196
                message = getattr(e, 'strerror', '')
197
                transaction.rollback()
198
            except IntegrityError, e:
199
                status = messages.ERROR
200
                message = _('There is already invitation for %s' % username)
201
                transaction.rollback()
202
        else:
203
            status = messages.ERROR
204
            message = _('No invitations left')
205
    messages.add_message(request, status, message)
206
    
207
    sent = [{'email': inv.username,
208
             'realname': inv.realname,
209
             'is_consumed': inv.is_consumed}
210
             for inv in inviter.invitations_sent.all()]
211
    kwargs = {'inviter': inviter,
212
              'sent':sent}
213
    context = get_context(request, extra_context, **kwargs)
214
    return render_response(template_name,
215
                           context_instance = context)
216

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

    
272
def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
273
    """
274
    Allows a user to create a local account.
275
    
276
    In case of GET request renders a form for providing the user information.
277
    In case of POST handles the signup.
278
    
279
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
280
    if present, otherwise to the ``astakos.im.backends.InvitationBackend``
281
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
282
    (see backends);
283
    
284
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
285
    otherwise renders the same page with a success message.
286
    
287
    On unsuccessful creation, renders ``on_failure`` with an error message.
288
    
289
    **Arguments**
290
    
291
    ``on_failure``
292
        A custom template to render in case of failure. This is optional;
293
        if not specified, this will default to ``im/signup.html``.
294
    
295
    
296
    ``on_success``
297
        A custom template to render in case of success. This is optional;
298
        if not specified, this will default to ``im/signup_complete.html``.
299
    
300
    ``extra_context``
301
        An dictionary of variables to add to the template context.
302
    
303
    **Template:**
304
    
305
    im/signup.html or ``on_failure`` keyword argument.
306
    im/signup_complete.html or ``on_success`` keyword argument. 
307
    """
308
    if request.user.is_authenticated():
309
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
310
    try:
311
        if not backend:
312
            backend = get_backend(request)
313
        for provider in IM_MODULES:
314
            extra_context['%s_form' % provider] = backend.get_signup_form(provider)
315
        if request.method == 'POST':
316
            provider = request.POST.get('provider')
317
            next = request.POST.get('next', '')
318
            form = extra_context['%s_form' % provider]
319
            if form.is_valid():
320
                if provider != 'local':
321
                    url = reverse('astakos.im.target.%s.login' % provider)
322
                    url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
323
                    if backend.invitation:
324
                        url = '%s&code=%s' % (url, backend.invitation.code)
325
                    return redirect(url)
326
                else:
327
                    status, message, user = backend.signup(form)
328
                    if user and user.is_active:
329
                        return prepare_response(request, user, next=next)
330
                    messages.add_message(request, status, message)
331
                    return render_response(on_success,
332
                                           context_instance=get_context(request, extra_context))
333
    except (Invitation.DoesNotExist, ValueError), e:
334
        messages.add_message(request, messages.ERROR, e)
335
        for provider in IM_MODULES:
336
            main = provider.capitalize() if provider == 'local' else 'ThirdParty'
337
            formclass = '%sUserCreationForm' % main
338
            extra_context['%s_form' % provider] = globals()[formclass]()
339
    return render_response(on_failure,
340
                           context_instance=get_context(request, extra_context))
341

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

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

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

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

    
490
@signed_terms_required
491
def change_password(request):
492
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))