Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ e015e9e6

History | View | Annotate | Download (16.5 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 random import randint
38
from smtplib import SMTPException
39
from urllib import quote
40
from functools import wraps
41

    
42
from django.core.mail import send_mail
43
from django.http import HttpResponse
44
from django.shortcuts import redirect
45
from django.template.loader import render_to_string
46
from django.utils.translation import ugettext as _
47
from django.core.urlresolvers import reverse
48
from django.contrib.auth.decorators import login_required
49
from django.contrib import messages
50
from django.db import transaction
51
from django.contrib.auth import logout as auth_logout
52
from django.utils.http import urlencode
53
from django.http import HttpResponseRedirect
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, get_current_site, prepare_response
58
from astakos.im.forms import *
59
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, IM_MODULES
60

    
61
logger = logging.getLogger(__name__)
62

    
63
def render_response(template, tab=None, status=200, 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
    return HttpResponse(html, status=status)
74

    
75

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

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

    
123
def _generate_invitation_code():
124
    while True:
125
        code = randint(1, 2L**63 - 1)
126
        try:
127
            Invitation.objects.get(code=code)
128
            # An invitation with this code already exists, try again
129
        except Invitation.DoesNotExist:
130
            return code
131

    
132
def _send_invitation(request, baseurl, inv):
133
    sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
134
    subject = _('Invitation to %s' % sitename)
135
    baseurl = request.build_absolute_uri('/').rstrip('/')
136
    url = '%s%s?code=%d' % (baseurl, reverse('astakos.im.views.signup'), inv.code)
137
    message = render_to_string('im/invitation.txt', {
138
                'invitation': inv,
139
                'url': url,
140
                'baseurl': baseurl,
141
                'service': sitename,
142
                'support': DEFAULT_CONTACT_EMAIL % sitename.lower()})
143
    sender = DEFAULT_FROM_EMAIL % sitename
144
    send_mail(subject, message, sender, [inv.username])
145
    logger.info('Sent invitation %s', inv)
146

    
147
@login_required
148
@transaction.commit_manually
149
def invite(request, template_name='im/invitations.html', extra_context={}):
150
    """
151
    Allows a user to invite somebody else.
152
    
153
    In case of GET request renders a form for providing the invitee information.
154
    In case of POST checks whether the user has not run out of invitations and then
155
    sends an invitation email to singup to the service.
156
    
157
    The view uses commit_manually decorator in order to ensure the number of the
158
    user invitations is going to be updated only if the email has been successfully sent.
159
    
160
    If the user isn't logged in, redirects to settings.LOGIN_URL.
161
    
162
    **Arguments**
163
    
164
    ``template_name``
165
        A custom template to use. This is optional; if not specified,
166
        this will default to ``im/invitations.html``.
167
    
168
    ``extra_context``
169
        An dictionary of variables to add to the template context.
170
    
171
    **Template:**
172
    
173
    im/invitations.html or ``template_name`` keyword argument.
174
    
175
    **Settings:**
176
    
177
    The view expectes the following settings are defined:
178
    
179
    * LOGIN_URL: login uri
180
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
181
    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
182
    """
183
    status = None
184
    message = None
185
    inviter = AstakosUser.objects.get(username = request.user.username)
186
    
187
    if request.method == 'POST':
188
        username = request.POST.get('uniq')
189
        realname = request.POST.get('realname')
190
        
191
        if inviter.invitations > 0:
192
            code = _generate_invitation_code()
193
            invitation = Invitation(inviter=inviter,
194
                                    username=username,
195
                                    code=code,
196
                                    realname=realname)
197
            invitation.save()
198
            
199
            try:
200
                baseurl = request.build_absolute_uri('/').rstrip('/')
201
                _send_invitation(request, baseurl, invitation)
202
                inviter.invitations = max(0, inviter.invitations - 1)
203
                inviter.save()
204
                status = messages.SUCCESS
205
                message = _('Invitation sent to %s' % username)
206
                transaction.commit()
207
            except (SMTPException, socket.error) as e:
208
                status = messages.ERROR
209
                message = getattr(e, 'strerror', '')
210
                transaction.rollback()
211
        else:
212
            status = messages.ERROR
213
            message = _('No invitations left')
214
    messages.add_message(request, status, message)
215
    
216
    sent = [{'email': inv.username,
217
             'realname': inv.realname,
218
             'is_consumed': inv.is_consumed}
219
             for inv in inviter.invitations_sent.all()]
220
    kwargs = {'inviter': inviter,
221
              'sent':sent}
222
    context = get_context(request, extra_context, **kwargs)
223
    return render_response(template_name,
224
                           context_instance = context)
225

    
226
@login_required
227
def edit_profile(request, template_name='im/profile.html', extra_context={}):
228
    """
229
    Allows a user to edit his/her profile.
230
    
231
    In case of GET request renders a form for displaying the user information.
232
    In case of POST updates the user informantion and redirects to ``next``
233
    url parameter if exists.
234
    
235
    If the user isn't logged in, redirects to settings.LOGIN_URL.
236
    
237
    **Arguments**
238
    
239
    ``template_name``
240
        A custom template to use. This is optional; if not specified,
241
        this will default to ``im/profile.html``.
242
    
243
    ``extra_context``
244
        An dictionary of variables to add to the template context.
245
    
246
    **Template:**
247
    
248
    im/profile.html or ``template_name`` keyword argument.
249
    
250
    **Settings:**
251
    
252
    The view expectes the following settings are defined:
253
    
254
    * LOGIN_URL: login uri
255
    """
256
    form = ProfileForm(instance=request.user)
257
    extra_context['next'] = request.GET.get('next')
258
    if request.method == 'POST':
259
        form = ProfileForm(request.POST, instance=request.user)
260
        if form.is_valid():
261
            try:
262
                form.save()
263
                next = request.POST.get('next')
264
                if next:
265
                    return redirect(next)
266
                msg = _('Profile has been updated successfully')
267
                messages.add_message(request, messages.SUCCESS, msg)
268
            except ValueError, ve:
269
                messages.add_message(request, messages.ERROR, ve)
270
    return render_response(template_name,
271
                           form = form,
272
                           context_instance = get_context(request,
273
                                                          extra_context,
274
                                                          user=request.user))
275

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

    
344
@login_required
345
def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
346
    """
347
    Allows a user to send feedback.
348
    
349
    In case of GET request renders a form for providing the feedback information.
350
    In case of POST sends an email to support team.
351
    
352
    If the user isn't logged in, redirects to settings.LOGIN_URL.
353
    
354
    **Arguments**
355
    
356
    ``template_name``
357
        A custom template to use. This is optional; if not specified,
358
        this will default to ``im/feedback.html``.
359
    
360
    ``extra_context``
361
        An dictionary of variables to add to the template context.
362
    
363
    **Template:**
364
    
365
    im/signup.html or ``template_name`` keyword argument.
366
    
367
    **Settings:**
368
    
369
    * LOGIN_URL: login uri
370
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
371
    """
372
    if request.method == 'GET':
373
        form = FeedbackForm()
374
    if request.method == 'POST':
375
        if not request.user:
376
            return HttpResponse('Unauthorized', status=401)
377
        
378
        form = FeedbackForm(request.POST)
379
        if form.is_valid():
380
            sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
381
            subject = _("Feedback from %s" % sitename)
382
            from_email = request.user.email
383
            recipient_list = [DEFAULT_CONTACT_EMAIL % sitename.lower()]
384
            content = render_to_string(email_template_name, {
385
                        'message': form.cleaned_data['feedback_msg'],
386
                        'data': form.cleaned_data['feedback_data'],
387
                        'request': request})
388
            
389
            try:
390
                send_mail(subject, content, from_email, recipient_list)
391
                message = _('Feedback successfully sent')
392
                status = messages.SUCCESS
393
            except (SMTPException, socket.error) as e:
394
                status = messages.ERROR
395
                message = getattr(e, 'strerror', '')
396
            messages.add_message(request, status, message)
397
    return render_response(template_name,
398
                           form = form,
399
                           context_instance = get_context(request, extra_context))
400

    
401
def logout(request, template='registration/logged_out.html', extra_context={}):
402
    """
403
    Wraps `django.contrib.auth.logout` and delete the cookie.
404
    """
405
    auth_logout(request)
406
    response = HttpResponse()
407
    response.delete_cookie(COOKIE_NAME)
408
    next = request.GET.get('next')
409
    if next:
410
        response['Location'] = next
411
        response.status_code = 302
412
        return response
413
    context = get_context(request, extra_context)
414
    response.write(render_to_string(template, context_instance=context))
415
    return response
416

    
417
def activate(request):
418
    """
419
    Activates the user identified by the ``auth`` request parameter
420
    """
421
    token = request.GET.get('auth')
422
    next = request.GET.get('next')
423
    try:
424
        user = AstakosUser.objects.get(auth_token=token)
425
    except AstakosUser.DoesNotExist:
426
        return HttpResponseBadRequest('No such user')
427
    
428
    user.is_active = True
429
    user.save()
430
    return prepare_response(request, user, next, renew=True)