Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ 7482228b

History | View | Annotate | Download (16.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 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
    url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(sitedomain))
134
    message = render_to_string('invitation.txt', {
135
                'invitation': inv,
136
                'url': url,
137
                'baseurl': baseurl,
138
                'service': sitename,
139
                'support': settings.DEFAULT_CONTACT_EMAIL % sitename.lower()})
140
    sender = settings.DEFAULT_FROM_EMAIL % sitename
141
    send_mail(subject, message, sender, [inv.username])
142
    logging.info('Sent invitation %s', inv)
143

    
144
@login_required
145
@transaction.commit_manually
146
def invite(request, template_name='invitations.html', extra_context={}):
147
    """
148
    Allows a user to invite somebody else.
149
    
150
    In case of GET request renders a form for providing the invitee information.
151
    In case of POST checks whether the user has not run out of invitations and then
152
    sends an invitation email to singup to the service.
153
    
154
    The view uses commit_manually decorator in order to ensure the number of the
155
    user invitations is going to be updated only if the email has been successfully sent.
156
    
157
    If the user isn't logged in, redirects to settings.LOGIN_URL.
158
    
159
    **Arguments**
160
    
161
    ``template_name``
162
        A custom template to use. This is optional; if not specified,
163
        this will default to ``invitations.html``.
164
    
165
    ``extra_context``
166
        An dictionary of variables to add to the template context.
167
    
168
    **Template:**
169
    
170
    invitations.html or ``template_name`` keyword argument.
171
    
172
    **Settings:**
173
    
174
    The view expectes the following settings are defined:
175
    
176
    * LOGIN_URL: login uri
177
    * SIGNUP_TARGET: Where users should signup with their invitation code
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, created = Invitation.objects.get_or_create(
192
                inviter=inviter,
193
                username=username,
194
                defaults={'code': code, 'realname': realname})
195
            
196
            try:
197
                baseurl = request.build_absolute_uri('/').rstrip('/')
198
                _send_invitation(request, baseurl, invitation)
199
                if created:
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_accepted': inv.is_accepted}
217
                    for inv in inviter.invitations_sent.all()]
218
    kwargs = {'user': 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
                msg = _('Profile has been updated successfully')
256
                messages.add_message(request, messages.SUCCESS, msg)
257
            except ValueError, ve:
258
                messages.add_message(request, messages.ERROR, ve)
259
        next = request.POST.get('next')
260
        if next:
261
            return redirect(next)
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, template_name='signup.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
    ``template_name``
288
        A custom template to use. This is optional; if not specified,
289
        this will default to ``signup.html``.
290
    
291
    ``extra_context``
292
        An dictionary of variables to add to the template context.
293
    
294
    **Template:**
295
    
296
    signup.html or ``template_name`` keyword argument.
297
    """
298
    try:
299
        if not backend:
300
            backend = get_backend(request)
301
        for provider in settings.IM_MODULES:
302
            extra_context['%s_form' % provider] = backend.get_signup_form(provider)
303
        if request.method == 'POST':
304
            provider = request.POST.get('provider')
305
            next = request.POST.get('next', '')
306
            form = extra_context['%s_form' % provider]
307
            if form.is_valid():
308
                if provider != 'local':
309
                    url = reverse('astakos.im.target.%s.login' % provider)
310
                    url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
311
                    if backend.invitation:
312
                        url = '%s&code=%s' % (url, backend.invitation.code)
313
                    return redirect(url)
314
                else:
315
                    status, message, user = backend.signup(form)
316
                    if user and user.is_active:
317
                        return prepare_response(request, user, next=next)
318
                    messages.add_message(request, status, message)    
319
    except (Invitation.DoesNotExist, ValueError), e:
320
        messages.add_message(request, messages.ERROR, e)
321
        for provider in settings.IM_MODULES:
322
            main = provider.capitalize() if provider == 'local' else 'ThirdParty'
323
            formclass = '%sUserCreationForm' % main
324
            extra_context['%s_form' % provider] = globals()[formclass]()
325
    return render_response(template_name,
326
                           context_instance=get_context(request, extra_context))
327

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

    
384
def create_user(request, form, backend=None, post_data={}, next = None, template_name='login.html', extra_context={}): 
385
    try:
386
        if not backend:
387
            backend = get_backend(request)
388
        if form.is_valid():
389
            status, message, user = backend.signup(form)
390
            if status == messages.SUCCESS:
391
                for k,v in post_data.items():
392
                    setattr(user,k, v)
393
                user.save()
394
                if user.is_active():
395
                    return prepare_response(request, user, next=next)
396
            messages.add_message(request, status, message)
397
        else:
398
            messages.add_message(request, messages.ERROR, form.errors)
399
    except (Invitation.DoesNotExist, ValueError), e:
400
        messages.add_message(request, messages.ERROR, e)
401
    return render_response(template_name,
402
                           form = LocalUserCreationForm(),
403
                           context_instance=get_context(request, extra_context))
404

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