views.py 16.2 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
# 
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
# 
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# 
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

import logging
import socket

from random import randint
from smtplib import SMTPException
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
39
from urllib import quote
40
from functools import wraps
41
42
43

from django.conf import settings
from django.core.mail import send_mail
44
from django.http import HttpResponse
45
46
47
48
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
49
50
51
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db import transaction
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
52
from django.contrib.auth import logout as auth_logout
53
54
from django.utils.http import urlencode
from django.http import HttpResponseRedirect
55

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
56
57
from astakos.im.models import AstakosUser, Invitation
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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
61
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
62
63
64
65
66
    """
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
    keyword argument and returns an ``django.http.HttpResponse`` with the
    specified ``status``.
    """
67
    if tab is None:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
68
        tab = template.partition('_')[0].partition('.html')[0]
69
    kwargs.setdefault('tab', tab)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70
    html = render_to_string(template, kwargs, context_instance=context_instance)
71
72
    return HttpResponse(html, status=status)

73
74
75
76

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

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

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

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

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

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
393
def logout(request, template='registration/logged_out.html', extra_context={}):
394
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
395
    Wraps `django.contrib.auth.logout` and delete the cookie.
396
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
397
398
    auth_logout(request)
    response = HttpResponse()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
399
    response.delete_cookie(settings.COOKIE_NAME)
400
401
402
403
404
    next = request.GET.get('next')
    if next:
        response['Location'] = next
        response.status_code = 302
        return response
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
405
    html = render_to_string(template, context_instance=get_context(request, extra_context))
406
    return HttpResponse(html)
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421

def activate(request):
    """
    Activates the user identified by the ``auth`` request parameter
    """
    token = request.GET.get('auth')
    next = request.GET.get('next')
    try:
        user = AstakosUser.objects.get(auth_token=token)
    except AstakosUser.DoesNotExist:
        return HttpResponseBadRequest('No such user')
    
    user.is_active = True
    user.save()
    return prepare_response(request, user, next, renew=True)