Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 683cf244

History | View | Annotate | Download (16.6 kB)

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

    
34
import logging
35
import socket
36

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

    
41
from django.core.mail import send_mail
42
from django.http import HttpResponse
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

    
55
from astakos.im.models import AstakosUser, Invitation
56
from astakos.im.backends import get_backend
57
from astakos.im.util import get_context, prepare_response, set_cookie
58
from astakos.im.forms import *
59
from astakos.im.functions import send_greeting
60
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
61
from astakos.im.functions import invite as invite_func
62

    
63
logger = logging.getLogger(__name__)
64

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

    
80

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

    
95
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
96
    """
97
    If there is logged on user renders the profile page otherwise renders login page.
98
    
99
    **Arguments**
100
    
101
    ``login_template_name``
102
        A custom login template to use. This is optional; if not specified,
103
        this will default to ``im/login.html``.
104
    
105
    ``profile_template_name``
106
        A custom profile template to use. This is optional; if not specified,
107
        this will default to ``im/profile.html``.
108
    
109
    ``extra_context``
110
        An dictionary of variables to add to the template context.
111
    
112
    **Template:**
113
    
114
    im/profile.html or im/login.html or ``template_name`` keyword argument.
115
    
116
    """
117
    template_name = login_template_name
118
    formclass = 'LoginForm'
119
    kwargs = {}
120
    if request.user.is_authenticated():
121
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
122
    return render_response(template_name,
123
                           form = globals()[formclass](**kwargs),
124
                           context_instance = get_context(request, extra_context))
125

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

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

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

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

    
379
def logout(request, template='registration/logged_out.html', extra_context={}):
380
    """
381
    Wraps `django.contrib.auth.logout` and delete the cookie.
382
    """
383
    auth_logout(request)
384
    response = HttpResponse()
385
    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
386
    next = request.GET.get('next')
387
    if next:
388
        response['Location'] = next
389
        response.status_code = 302
390
        return response
391
    elif LOGOUT_NEXT:
392
        response['Location'] = LOGOUT_NEXT
393
        response.status_code = 301
394
        return response
395
    messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
396
    context = get_context(request, extra_context)
397
    response.write(render_to_string(template, context_instance=context))
398
    return response
399

    
400
@transaction.commit_manually
401
def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
402
    """
403
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
404
    and renews the user token.
405
    
406
    The view uses commit_manually decorator in order to ensure the user state will be updated
407
    only if the email will be send successfully.
408
    """
409
    token = request.GET.get('auth')
410
    next = request.GET.get('next')
411
    try:
412
        user = AstakosUser.objects.get(auth_token=token)
413
    except AstakosUser.DoesNotExist:
414
        return HttpResponseBadRequest(_('No such user'))
415
    
416
    user.is_active = True
417
    user.email_verified = True
418
    user.save()
419
    try:
420
        send_greeting(user, email_template_name)
421
        response = prepare_response(request, user, next, renew=True)
422
        transaction.commit()
423
        return response
424
    except (SMTPException, socket.error) as e:
425
        message = getattr(e, 'name') if hasattr(e, 'name') else e
426
        messages.add_message(request, messages.ERROR, message)
427
        transaction.rollback()
428
        return signup(request, on_failure='im/signup.html')