Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 8f5a3a06

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

    
41
from django.core.mail import send_mail
42
from django.http import HttpResponse
43
from django.shortcuts import redirect
44
from django.template.loader import render_to_string
45
from django.utils.translation import ugettext as _
46
from django.core.urlresolvers import reverse
47
from django.contrib.auth.decorators import login_required
48
from django.contrib import messages
49
from django.db import transaction
50
from django.contrib.auth import logout as auth_logout
51
from django.utils.http import urlencode
52
from django.http import HttpResponseRedirect, HttpResponseBadRequest
53
from django.db.utils import IntegrityError
54
from django.contrib.auth.views import password_change
55

    
56
from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
57
from astakos.im.activation_backends import get_backend
58
from astakos.im.util import get_context, prepare_response, set_cookie, has_signed_terms
59
from astakos.im.forms import *
60
from astakos.im.functions import send_greeting, send_feedback, SendMailError
61
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
62
from astakos.im.functions import invite as invite_func
63

    
64
logger = logging.getLogger(__name__)
65

    
66
def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
67
    """
68
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
69
    keyword argument and returns an ``django.http.HttpResponse`` with the
70
    specified ``status``.
71
    """
72
    if tab is None:
73
        tab = template.partition('_')[0].partition('.html')[0]
74
    kwargs.setdefault('tab', tab)
75
    html = render_to_string(template, kwargs, context_instance=context_instance)
76
    response = HttpResponse(html, status=status)
77
    if reset_cookie:
78
        set_cookie(response, context_instance['request'].user)
79
    return response
80

    
81

    
82
def requires_anonymous(func):
83
    """
84
    Decorator checkes whether the request.user is not Anonymous and in that case
85
    redirects to `logout`.
86
    """
87
    @wraps(func)
88
    def wrapper(request, *args):
89
        if not request.user.is_anonymous():
90
            next = urlencode({'next': request.build_absolute_uri()})
91
            logout_uri = reverse(logout) + '?' + next
92
            return HttpResponseRedirect(logout_uri)
93
        return func(request, *args)
94
    return wrapper
95

    
96
def signed_terms_required(func):
97
    """
98
    Decorator checkes whether the request.user is Anonymous and in that case
99
    redirects to `logout`.
100
    """
101
    @wraps(func)
102
    def wrapper(request, *args, **kwargs):
103
        if request.user.is_authenticated() and not has_signed_terms(request.user):
104
            params = urlencode({'next': request.build_absolute_uri(),
105
                              'show_form':''})
106
            terms_uri = reverse('latest_terms') + '?' + params
107
            return HttpResponseRedirect(terms_uri)
108
        return func(request, *args, **kwargs)
109
    return wrapper
110

    
111
@signed_terms_required
112
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
113
    """
114
    If there is logged on user renders the profile page otherwise renders login page.
115
    
116
    **Arguments**
117
    
118
    ``login_template_name``
119
        A custom login template to use. This is optional; if not specified,
120
        this will default to ``im/login.html``.
121
    
122
    ``profile_template_name``
123
        A custom profile template to use. This is optional; if not specified,
124
        this will default to ``im/profile.html``.
125
    
126
    ``extra_context``
127
        An dictionary of variables to add to the template context.
128
    
129
    **Template:**
130
    
131
    im/profile.html or im/login.html or ``template_name`` keyword argument.
132
    
133
    """
134
    template_name = login_template_name
135
    if request.user.is_authenticated():
136
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
137
    return render_response(template_name,
138
                           login_form = LoginForm(),
139
                           context_instance = get_context(request, extra_context))
140

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

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

    
269
@transaction.commit_manually
270
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
271
    """
272
    Allows a user to create a local account.
273
    
274
    In case of GET request renders a form for providing the user information.
275
    In case of POST handles the signup.
276
    
277
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
278
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
279
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
280
    (see activation_backends);
281
    
282
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
283
    otherwise renders the same page with a success message.
284
    
285
    On unsuccessful creation, renders ``template_name`` with an error message.
286
    
287
    **Arguments**
288
    
289
    ``template_name``
290
        A custom template to render. This is optional;
291
        if not specified, this will default to ``im/signup.html``.
292
    
293
    
294
    ``on_success``
295
        A custom template to render in case of success. This is optional;
296
        if not specified, this will default to ``im/signup_complete.html``.
297
    
298
    ``extra_context``
299
        An dictionary of variables to add to the template context.
300
    
301
    **Template:**
302
    
303
    im/signup.html or ``template_name`` keyword argument.
304
    im/signup_complete.html or ``on_success`` keyword argument. 
305
    """
306
    if request.user.is_authenticated():
307
        return HttpResponseRedirect(reverse('astakos.im.views.index'))
308
    if not backend:
309
        backend = get_backend(request)
310
    try:
311
        query_dict = request.__getattribute__(request.method)
312
        provider = query_dict.get('provider', 'local')
313
        form = backend.get_signup_form(provider)
314
    except (Invitation.DoesNotExist, ValueError), e:
315
        messages.add_message(request, messages.ERROR, e)
316
    if request.method == 'POST':
317
        if form.is_valid():
318
            user = form.save()
319
            try:
320
                result = backend.handle_activation(user)
321
            except SendMailError, e:
322
                message = e.message
323
                status = messages.ERROR
324
                transaction.rollback()
325
            else:
326
                message = result.message
327
                status = messages.SUCCESS
328
                transaction.commit()
329
                if user and user.is_active:
330
                    next = request.POST.get('next', '')
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
    return render_response(template_name,
336
                           local_signup_form = form,
337
                           context_instance=get_context(request, extra_context))
338

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

    
391
def logout(request, template='registration/logged_out.html', extra_context={}):
392
    """
393
    Wraps `django.contrib.auth.logout` and delete the cookie.
394
    """
395
    auth_logout(request)
396
    response = HttpResponse()
397
    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
398
    next = request.GET.get('next')
399
    if next:
400
        response['Location'] = next
401
        response.status_code = 302
402
        return response
403
    elif LOGOUT_NEXT:
404
        response['Location'] = LOGOUT_NEXT
405
        response.status_code = 301
406
        return response
407
    messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
408
    context = get_context(request, extra_context)
409
    response.write(render_to_string(template, context_instance=context))
410
    return response
411

    
412
@transaction.commit_manually
413
def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
414
    """
415
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
416
    and renews the user token.
417
    
418
    The view uses commit_manually decorator in order to ensure the user state will be updated
419
    only if the email will be send successfully.
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.email_verified = True
430
    user.save()
431
    try:
432
        send_greeting(user, email_template_name)
433
        response = prepare_response(request, user, next, renew=True)
434
        transaction.commit()
435
        return response
436
    except SendEmailError, e:
437
        message = e.message
438
        messages.add_message(request, messages.ERROR, message)
439
        transaction.rollback()
440
        return signup(request, on_failure='im/signup.html')
441

    
442
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
443
    term = None
444
    terms = None
445
    if not term_id:
446
        try:
447
            term = ApprovalTerms.objects.order_by('-id')[0]
448
        except IndexError:
449
            pass
450
    else:
451
        try:
452
             term = ApprovalTerms.objects.get(id=term_id)
453
        except ApprovalTermDoesNotExist, e:
454
            pass
455
    
456
    if not term:
457
        return HttpResponseBadRequest(_('No approval terms found.'))
458
    f = open(term.location, 'r')
459
    terms = f.read()
460
    
461
    if request.method == 'POST':
462
        next = request.POST.get('next')
463
        if not next:
464
            next = reverse('astakos.im.views.index')
465
        form = SignApprovalTermsForm(request.POST, instance=request.user)
466
        if not form.is_valid():
467
            return render_response(template_name,
468
                           terms = terms,
469
                           approval_terms_form = form,
470
                           context_instance = get_context(request, extra_context))
471
        user = form.save()
472
        return HttpResponseRedirect(next)
473
    else:
474
        form = None
475
        if request.user.is_authenticated() and not has_signed_terms(request.user):
476
            form = SignApprovalTermsForm(instance=request.user)
477
        return render_response(template_name,
478
                               terms = terms,
479
                               approval_terms_form = form,
480
                               context_instance = get_context(request, extra_context))
481

    
482
@signed_terms_required
483
def change_password(request):
484
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))