Statistics
| Branch: | Tag: | Revision:

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

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

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

    
374
def logout(request, template='registration/logged_out.html', extra_context={}):
375
    """
376
    Wraps `django.contrib.auth.logout` and delete the cookie.
377
    """
378
    auth_logout(request)
379
    response = HttpResponse()
380
    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
381
    next = request.GET.get('next')
382
    if next:
383
        response['Location'] = next
384
        response.status_code = 302
385
        return response
386
    context = get_context(request, extra_context)
387
    response.write(render_to_string(template, context_instance=context))
388
    return response
389

    
390
def activate(request):
391
    """
392
    Activates the user identified by the ``auth`` request parameter
393
    """
394
    token = request.GET.get('auth')
395
    next = request.GET.get('next')
396
    try:
397
        user = AstakosUser.objects.get(auth_token=token)
398
    except AstakosUser.DoesNotExist:
399
        return HttpResponseBadRequest('No such user')
400
    
401
    user.is_active = True
402
    user.save()
403
    return prepare_response(request, user, next, renew=True)