Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 3a9f4931

History | View | Annotate | Download (15.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
53

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

    
61
logger = logging.getLogger(__name__)
62

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

    
78

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

    
93
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
94
    """
95
    If there is logged on user renders the profile page otherwise renders login page.
96
    
97
    **Arguments**
98
    
99
    ``login_template_name``
100
        A custom login template to use. This is optional; if not specified,
101
        this will default to ``im/login.html``.
102
    
103
    ``profile_template_name``
104
        A custom profile template to use. This is optional; if not specified,
105
        this will default to ``im/profile.html``.
106
    
107
    ``extra_context``
108
        An dictionary of variables to add to the template context.
109
    
110
    **Template:**
111
    
112
    im/profile.html or im/login.html or ``template_name`` keyword argument.
113
    
114
    """
115
    template_name = login_template_name
116
    formclass = 'LoginForm'
117
    kwargs = {}
118
    if request.user.is_authenticated():
119
        template_name = profile_template_name
120
        formclass = 'ProfileForm'
121
        kwargs.update({'instance':request.user})
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
        else:
181
            status = messages.ERROR
182
            message = _('No invitations left')
183
    messages.add_message(request, status, message)
184
    
185
    sent = [{'email': inv.username,
186
             'realname': inv.realname,
187
             'is_consumed': inv.is_consumed}
188
             for inv in inviter.invitations_sent.all()]
189
    kwargs = {'inviter': inviter,
190
              'sent':sent}
191
    context = get_context(request, extra_context, **kwargs)
192
    return render_response(template_name,
193
                           context_instance = context)
194

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

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

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

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

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