Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ 2cbaacd5

History | View | Annotate | Download (16.4 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
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
62
    """
63
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
64
    keyword argument and returns an ``django.http.HttpResponse`` with the
65
    specified ``status``.
66
    """
67
    if tab is None:
68
        tab = template.partition('_')[0].partition('.html')[0]
69
    kwargs.setdefault('tab', tab)
70
    html = render_to_string(template, kwargs, context_instance=context_instance)
71
    return HttpResponse(html, status=status)
72

    
73

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

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

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

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

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

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

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

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

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

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