1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
37 from smtplib import SMTPException
38 from urllib import quote
39 from functools import wraps
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
56 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
57 from astakos.im.activation_backends import get_backend, SimpleBackend
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, LOGOUT_NEXT
62 from astakos.im.functions import invite as invite_func
64 logger = logging.getLogger(__name__)
66 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
68 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
69 keyword argument and returns an ``django.http.HttpResponse`` with the
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)
78 set_cookie(response, context_instance['request'].user)
82 def requires_anonymous(func):
84 Decorator checkes whether the request.user is not Anonymous and in that case
85 redirects to `logout`.
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)
96 def signed_terms_required(func):
98 Decorator checkes whether the request.user is Anonymous and in that case
99 redirects to `logout`.
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(),
106 terms_uri = reverse('latest_terms') + '?' + params
107 return HttpResponseRedirect(terms_uri)
108 return func(request, *args, **kwargs)
111 @signed_terms_required
112 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
114 If there is logged on user renders the profile page otherwise renders login page.
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``.
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``.
127 An dictionary of variables to add to the template context.
131 im/profile.html or im/login.html or ``template_name`` keyword argument.
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))
142 @signed_terms_required
143 @transaction.commit_manually
144 def invite(request, template_name='im/invitations.html', extra_context={}):
146 Allows a user to invite somebody else.
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.
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.
155 If the user isn't logged in, redirects to settings.LOGIN_URL.
160 A custom template to use. This is optional; if not specified,
161 this will default to ``im/invitations.html``.
164 An dictionary of variables to add to the template context.
168 im/invitations.html or ``template_name`` keyword argument.
172 The view expectes the following settings are defined:
174 * LOGIN_URL: login uri
175 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
176 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
180 form = InvitationForm()
182 inviter = request.user
183 if request.method == 'POST':
184 form = InvitationForm(request.POST)
186 if inviter.invitations > 0:
189 invitation = form.save()
190 invite_func(invitation, inviter)
191 status = messages.SUCCESS
192 message = _('Invitation sent to %s' % invitation.username)
193 except SendMailError, e:
194 status = messages.ERROR
196 transaction.rollback()
197 except BaseException, e:
198 status = messages.ERROR
199 message = _('Something went wrong.')
201 transaction.rollback()
205 status = messages.ERROR
206 message = _('No invitations left')
207 messages.add_message(request, status, message)
209 sent = [{'email': inv.username,
210 'realname': inv.realname,
211 'is_consumed': inv.is_consumed}
212 for inv in request.user.invitations_sent.all()]
213 kwargs = {'inviter': inviter,
215 context = get_context(request, extra_context, **kwargs)
216 return render_response(template_name,
217 invitation_form = form,
218 context_instance = context)
221 @signed_terms_required
222 def edit_profile(request, template_name='im/profile.html', extra_context={}):
224 Allows a user to edit his/her profile.
226 In case of GET request renders a form for displaying the user information.
227 In case of POST updates the user informantion and redirects to ``next``
228 url parameter if exists.
230 If the user isn't logged in, redirects to settings.LOGIN_URL.
235 A custom template to use. This is optional; if not specified,
236 this will default to ``im/profile.html``.
239 An dictionary of variables to add to the template context.
243 im/profile.html or ``template_name`` keyword argument.
247 The view expectes the following settings are defined:
249 * LOGIN_URL: login uri
251 form = ProfileForm(instance=request.user)
252 extra_context['next'] = request.GET.get('next')
254 if request.method == 'POST':
255 form = ProfileForm(request.POST, instance=request.user)
258 prev_token = request.user.auth_token
260 reset_cookie = user.auth_token != prev_token
261 form = ProfileForm(instance=user)
262 next = request.POST.get('next')
264 return redirect(next)
265 msg = _('Profile has been updated successfully')
266 messages.add_message(request, messages.SUCCESS, msg)
267 except ValueError, ve:
268 messages.add_message(request, messages.ERROR, ve)
269 return render_response(template_name,
270 reset_cookie = reset_cookie,
272 context_instance = get_context(request,
275 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
277 Allows a user to create a local account.
279 In case of GET request renders a form for providing the user information.
280 In case of POST handles the signup.
282 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
283 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
284 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
285 (see activation_backends);
287 Upon successful user creation if ``next`` url parameter is present the user is redirected there
288 otherwise renders the same page with a success message.
290 On unsuccessful creation, renders ``template_name`` with an error message.
295 A custom template to render. This is optional;
296 if not specified, this will default to ``im/signup.html``.
300 A custom template to render in case of success. This is optional;
301 if not specified, this will default to ``im/signup_complete.html``.
304 An dictionary of variables to add to the template context.
308 im/signup.html or ``template_name`` keyword argument.
309 im/signup_complete.html or ``on_success`` keyword argument.
311 if request.user.is_authenticated():
312 return HttpResponseRedirect(reverse('astakos.im.views.index'))
314 query_dict = request.__getattribute__(request.method)
315 provider = query_dict.get('provider', 'local')
318 backend = get_backend(request)
319 form = backend.get_signup_form(provider)
320 except (Invitation.DoesNotExist, ValueError), e:
321 form = SimpleBackend(request).get_signup_form(provider)
322 messages.add_message(request, messages.ERROR, e)
323 if request.method == 'POST':
325 user = form.save(commit=False)
327 result = backend.handle_activation(user)
328 status = messages.SUCCESS
329 message = result.message
331 if user and user.is_active:
332 next = request.POST.get('next', '')
333 return prepare_response(request, user, next=next)
334 messages.add_message(request, status, message)
335 return render_response(on_success,
336 context_instance=get_context(request, extra_context))
337 except SendMailError, e:
338 status = messages.ERROR
340 messages.add_message(request, status, message)
341 except BaseException, e:
342 status = messages.ERROR
343 message = _('Something went wrong.')
344 messages.add_message(request, status, message)
345 return render_response(template_name,
347 context_instance=get_context(request, extra_context))
350 @signed_terms_required
351 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
353 Allows a user to send feedback.
355 In case of GET request renders a form for providing the feedback information.
356 In case of POST sends an email to support team.
358 If the user isn't logged in, redirects to settings.LOGIN_URL.
363 A custom template to use. This is optional; if not specified,
364 this will default to ``im/feedback.html``.
367 An dictionary of variables to add to the template context.
371 im/signup.html or ``template_name`` keyword argument.
375 * LOGIN_URL: login uri
376 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
378 if request.method == 'GET':
379 form = FeedbackForm()
380 if request.method == 'POST':
382 return HttpResponse('Unauthorized', status=401)
384 form = FeedbackForm(request.POST)
386 msg = form.cleaned_data['feedback_msg'],
387 data = form.cleaned_data['feedback_data']
389 send_feedback(msg, data, request.user, email_template_name)
390 except SendMailError, e:
392 status = messages.ERROR
394 message = _('Feedback successfully sent')
395 status = messages.SUCCESS
396 messages.add_message(request, status, message)
397 return render_response(template_name,
398 feedback_form = form,
399 context_instance = get_context(request, extra_context))
401 def logout(request, template='registration/logged_out.html', extra_context={}):
403 Wraps `django.contrib.auth.logout` and delete the cookie.
406 response = HttpResponse()
407 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
408 next = request.GET.get('next')
410 response['Location'] = next
411 response.status_code = 302
414 response['Location'] = LOGOUT_NEXT
415 response.status_code = 301
417 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
418 context = get_context(request, extra_context)
419 response.write(render_to_string(template, context_instance=context))
422 @transaction.commit_manually
423 def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
425 Activates the user identified by the ``auth`` request parameter, sends a welcome email
426 and renews the user token.
428 The view uses commit_manually decorator in order to ensure the user state will be updated
429 only if the email will be send successfully.
431 token = request.GET.get('auth')
432 next = request.GET.get('next')
434 user = AstakosUser.objects.get(auth_token=token)
435 except AstakosUser.DoesNotExist:
436 return HttpResponseBadRequest(_('No such user'))
438 user.is_active = True
439 user.email_verified = True
442 send_greeting(user, email_template_name)
443 response = prepare_response(request, user, next, renew=True)
446 except SendEmailError, e:
448 messages.add_message(request, messages.ERROR, message)
449 transaction.rollback()
450 return signup(request, on_failure='im/signup.html')
451 except BaseException, e:
452 status = messages.ERROR
453 message = _('Something went wrong.')
455 transaction.rollback()
456 return signup(request, on_failure='im/signup.html')
458 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
463 term = ApprovalTerms.objects.order_by('-id')[0]
468 term = ApprovalTerms.objects.get(id=term_id)
469 except ApprovalTermDoesNotExist, e:
473 return HttpResponseBadRequest(_('No approval terms found.'))
474 f = open(term.location, 'r')
477 if request.method == 'POST':
478 next = request.POST.get('next')
480 next = reverse('astakos.im.views.index')
481 form = SignApprovalTermsForm(request.POST, instance=request.user)
482 if not form.is_valid():
483 return render_response(template_name,
485 approval_terms_form = form,
486 context_instance = get_context(request, extra_context))
488 return HttpResponseRedirect(next)
491 if request.user.is_authenticated() and not has_signed_terms(request.user):
492 form = SignApprovalTermsForm(instance=request.user)
493 return render_response(template_name,
495 approval_terms_form = form,
496 context_instance = get_context(request, extra_context))
498 @signed_terms_required
499 def change_password(request):
500 return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))