Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 8316698a

History | View | Annotate | Download (15.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
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.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
60
from astakos.im.functions import invite as invite_func
61

    
62
logger = logging.getLogger(__name__)
63

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

    
79

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

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

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

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

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

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

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

    
401
def activate(request):
402
    """
403
    Activates the user identified by the ``auth`` request parameter
404
    """
405
    token = request.GET.get('auth')
406
    next = request.GET.get('next')
407
    try:
408
        user = AstakosUser.objects.get(auth_token=token)
409
    except AstakosUser.DoesNotExist:
410
        return HttpResponseBadRequest('No such user')
411
    
412
    user.is_active = True
413
    user.email_verified = True
414
    user.save()
415
    return prepare_response(request, user, next, renew=True)