Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ 49df775e

History | View | Annotate | Download (16.2 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.conf import settings
43
from django.core.mail import send_mail
44
from django.http import HttpResponse
45
from django.shortcuts import redirect
46
from django.template.loader import render_to_string
47
from django.utils.translation import ugettext as _
48
from django.core.urlresolvers import reverse
49
from django.contrib.auth.decorators import login_required
50
from django.contrib import messages
51
from django.db import transaction
52
from django.contrib.auth import logout as auth_logout
53
from django.utils.http import urlencode
54
from django.http import HttpResponseRedirect
55

    
56
from astakos.im.models import AstakosUser, Invitation
57
from astakos.im.backends import get_backend
58
from astakos.im.util import get_context, get_current_site, prepare_response
59
from astakos.im.forms import *
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='login.html', profile_template_name='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 ``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 ``profile.html``.
101
    
102
    ``extra_context``
103
        An dictionary of variables to add to the template context.
104
    
105
    **Template:**
106
    
107
    profile.html or 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('invitation.txt', {
136
                'invitation': inv,
137
                'url': url,
138
                'baseurl': baseurl,
139
                'service': sitename,
140
                'support': settings.DEFAULT_CONTACT_EMAIL % sitename.lower()})
141
    sender = settings.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='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 ``invitations.html``.
165
    
166
    ``extra_context``
167
        An dictionary of variables to add to the template context.
168
    
169
    **Template:**
170
    
171
    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
    * DEFAULT_CONTACT_EMAIL: service support email
179
    * 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='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 ``profile.html``.
240
    
241
    ``extra_context``
242
        An dictionary of variables to add to the template context.
243
    
244
    **Template:**
245
    
246
    profile.html or ``template_name`` keyword argument.
247
    """
248
    form = ProfileForm(instance=request.user)
249
    extra_context['next'] = request.GET.get('next')
250
    if request.method == 'POST':
251
        form = ProfileForm(request.POST, instance=request.user)
252
        if form.is_valid():
253
            try:
254
                form.save()
255
                next = request.POST.get('next')
256
                if next:
257
                    return redirect(next)
258
                msg = _('Profile has been updated successfully')
259
                messages.add_message(request, messages.SUCCESS, msg)
260
            except ValueError, ve:
261
                messages.add_message(request, messages.ERROR, ve)
262
    return render_response(template_name,
263
                           form = form,
264
                           context_instance = get_context(request,
265
                                                          extra_context,
266
                                                          user=request.user))
267

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

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

    
392
def logout(request, template='registration/logged_out.html', extra_context={}):
393
    """
394
    Wraps `django.contrib.auth.logout` and delete the cookie.
395
    """
396
    auth_logout(request)
397
    response = HttpResponse()
398
    response.delete_cookie(settings.COOKIE_NAME)
399
    next = request.GET.get('next')
400
    if next:
401
        response['Location'] = next
402
        response.status_code = 302
403
        return response
404
    context = get_context(request, extra_context)
405
    response.write(render_to_string(template, context_instance=context))
406
    return response
407

    
408
def activate(request):
409
    """
410
    Activates the user identified by the ``auth`` request parameter
411
    """
412
    token = request.GET.get('auth')
413
    next = request.GET.get('next')
414
    try:
415
        user = AstakosUser.objects.get(auth_token=token)
416
    except AstakosUser.DoesNotExist:
417
        return HttpResponseBadRequest('No such user')
418
    
419
    user.is_active = True
420
    user.save()
421
    return prepare_response(request, user, next, renew=True)