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.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
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
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 formclass = 'LoginForm'
137 if request.user.is_authenticated():
138 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
139 return render_response(template_name,
140 form = globals()[formclass](**kwargs),
141 context_instance = get_context(request, extra_context))
144 @signed_terms_required
145 @transaction.commit_manually
146 def invite(request, template_name='im/invitations.html', extra_context={}):
148 Allows a user to invite somebody else.
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.
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.
157 If the user isn't logged in, redirects to settings.LOGIN_URL.
162 A custom template to use. This is optional; if not specified,
163 this will default to ``im/invitations.html``.
166 An dictionary of variables to add to the template context.
170 im/invitations.html or ``template_name`` keyword argument.
174 The view expectes the following settings are defined:
176 * LOGIN_URL: login uri
177 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
178 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
182 inviter = AstakosUser.objects.get(username = request.user.username)
184 if request.method == 'POST':
185 username = request.POST.get('uniq')
186 realname = request.POST.get('realname')
188 if inviter.invitations > 0:
190 invite_func(inviter, username, realname)
191 status = messages.SUCCESS
192 message = _('Invitation sent to %s' % username)
194 except (SMTPException, socket.error) as e:
195 status = messages.ERROR
196 message = getattr(e, 'strerror', '')
197 transaction.rollback()
198 except IntegrityError, e:
199 status = messages.ERROR
200 message = _('There is already invitation for %s' % username)
201 transaction.rollback()
203 status = messages.ERROR
204 message = _('No invitations left')
205 messages.add_message(request, status, message)
207 sent = [{'email': inv.username,
208 'realname': inv.realname,
209 'is_consumed': inv.is_consumed}
210 for inv in inviter.invitations_sent.all()]
211 kwargs = {'inviter': inviter,
213 context = get_context(request, extra_context, **kwargs)
214 return render_response(template_name,
215 context_instance = context)
218 @signed_terms_required
219 def edit_profile(request, template_name='im/profile.html', extra_context={}):
221 Allows a user to edit his/her profile.
223 In case of GET request renders a form for displaying the user information.
224 In case of POST updates the user informantion and redirects to ``next``
225 url parameter if exists.
227 If the user isn't logged in, redirects to settings.LOGIN_URL.
232 A custom template to use. This is optional; if not specified,
233 this will default to ``im/profile.html``.
236 An dictionary of variables to add to the template context.
240 im/profile.html or ``template_name`` keyword argument.
244 The view expectes the following settings are defined:
246 * LOGIN_URL: login uri
248 form = ProfileForm(instance=request.user)
249 extra_context['next'] = request.GET.get('next')
251 if request.method == 'POST':
252 form = ProfileForm(request.POST, instance=request.user)
255 prev_token = request.user.auth_token
257 reset_cookie = user.auth_token != prev_token
258 form = ProfileForm(instance=user)
259 next = request.POST.get('next')
261 return redirect(next)
262 msg = _('Profile has been updated successfully')
263 messages.add_message(request, messages.SUCCESS, msg)
264 except ValueError, ve:
265 messages.add_message(request, messages.ERROR, ve)
266 return render_response(template_name,
267 reset_cookie = reset_cookie,
269 context_instance = get_context(request,
272 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
274 Allows a user to create a local account.
276 In case of GET request renders a form for providing the user information.
277 In case of POST handles the signup.
279 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
280 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
281 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
284 Upon successful user creation if ``next`` url parameter is present the user is redirected there
285 otherwise renders the same page with a success message.
287 On unsuccessful creation, renders ``on_failure`` with an error message.
292 A custom template to render in case of failure. This is optional;
293 if not specified, this will default to ``im/signup.html``.
297 A custom template to render in case of success. This is optional;
298 if not specified, this will default to ``im/signup_complete.html``.
301 An dictionary of variables to add to the template context.
305 im/signup.html or ``on_failure`` keyword argument.
306 im/signup_complete.html or ``on_success`` keyword argument.
308 if request.user.is_authenticated():
309 return HttpResponseRedirect(reverse('astakos.im.views.index'))
312 backend = get_backend(request)
313 for provider in IM_MODULES:
314 extra_context['%s_form' % provider] = backend.get_signup_form(provider)
315 if request.method == 'POST':
316 provider = request.POST.get('provider')
317 next = request.POST.get('next', '')
318 form = extra_context['%s_form' % provider]
320 if provider != 'local':
321 url = reverse('astakos.im.target.%s.login' % provider)
322 url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
323 if backend.invitation:
324 url = '%s&code=%s' % (url, backend.invitation.code)
327 status, message, user = backend.signup(form)
328 if user and user.is_active:
329 return prepare_response(request, user, next=next)
330 messages.add_message(request, status, message)
331 return render_response(on_success,
332 context_instance=get_context(request, extra_context))
333 except (Invitation.DoesNotExist, ValueError), e:
334 messages.add_message(request, messages.ERROR, e)
335 for provider in IM_MODULES:
336 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
337 formclass = '%sUserCreationForm' % main
338 extra_context['%s_form' % provider] = globals()[formclass]()
339 return render_response(on_failure,
340 context_instance=get_context(request, extra_context))
343 @signed_terms_required
344 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
346 Allows a user to send feedback.
348 In case of GET request renders a form for providing the feedback information.
349 In case of POST sends an email to support team.
351 If the user isn't logged in, redirects to settings.LOGIN_URL.
356 A custom template to use. This is optional; if not specified,
357 this will default to ``im/feedback.html``.
360 An dictionary of variables to add to the template context.
364 im/signup.html or ``template_name`` keyword argument.
368 * LOGIN_URL: login uri
369 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
371 if request.method == 'GET':
372 form = FeedbackForm()
373 if request.method == 'POST':
375 return HttpResponse('Unauthorized', status=401)
377 form = FeedbackForm(request.POST)
379 subject = _("Feedback from %s alpha2 testing" % SITENAME)
380 from_email = request.user.email
381 recipient_list = [DEFAULT_CONTACT_EMAIL]
382 content = render_to_string(email_template_name, {
383 'message': form.cleaned_data['feedback_msg'],
384 'data': form.cleaned_data['feedback_data'],
388 send_mail(subject, content, from_email, recipient_list)
389 message = _('Feedback successfully sent')
390 status = messages.SUCCESS
391 except (SMTPException, socket.error) as e:
392 status = messages.ERROR
393 message = getattr(e, 'strerror', '')
394 messages.add_message(request, status, message)
395 return render_response(template_name,
397 context_instance = get_context(request, extra_context))
399 def logout(request, template='registration/logged_out.html', extra_context={}):
401 Wraps `django.contrib.auth.logout` and delete the cookie.
404 response = HttpResponse()
405 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
406 next = request.GET.get('next')
408 response['Location'] = next
409 response.status_code = 302
412 response['Location'] = LOGOUT_NEXT
413 response.status_code = 301
415 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
416 context = get_context(request, extra_context)
417 response.write(render_to_string(template, context_instance=context))
420 @transaction.commit_manually
421 def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
423 Activates the user identified by the ``auth`` request parameter, sends a welcome email
424 and renews the user token.
426 The view uses commit_manually decorator in order to ensure the user state will be updated
427 only if the email will be send successfully.
429 token = request.GET.get('auth')
430 next = request.GET.get('next')
432 user = AstakosUser.objects.get(auth_token=token)
433 except AstakosUser.DoesNotExist:
434 return HttpResponseBadRequest(_('No such user'))
436 user.is_active = True
437 user.email_verified = True
440 send_greeting(user, email_template_name)
441 response = prepare_response(request, user, next, renew=True)
444 except (SMTPException, socket.error) as e:
445 message = getattr(e, 'name') if hasattr(e, 'name') else e
446 messages.add_message(request, messages.ERROR, message)
447 transaction.rollback()
448 return signup(request, on_failure='im/signup.html')
450 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
455 term = ApprovalTerms.objects.order_by('-id')[0]
460 term = ApprovalTerms.objects.get(id=term_id)
461 except ApprovalTermDoesNotExist, e:
465 return HttpResponseBadRequest(_('No approval terms found.'))
466 f = open(term.location, 'r')
469 if request.method == 'POST':
470 next = request.POST.get('next')
472 next = reverse('astakos.im.views.index')
473 form = SignApprovalTermsForm(request.POST, instance=request.user)
474 if not form.is_valid():
475 return render_response(template_name,
478 context_instance = get_context(request, extra_context))
480 return HttpResponseRedirect(next)
483 if request.user.is_authenticated() and not has_signed_terms(request.user):
484 form = SignApprovalTermsForm(instance=request.user)
485 return render_response(template_name,
488 context_instance = get_context(request, extra_context))
490 @signed_terms_required
491 def change_password(request):
492 return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))