Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.8 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
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
121
    return render_response(template_name,
122
                           form = globals()[formclass](**kwargs),
123
                           context_instance = get_context(request, extra_context))
124

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

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

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

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

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

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