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
55 from astakos.im.models import AstakosUser, Invitation
56 from astakos.im.backends import get_backend
57 from astakos.im.util import get_context, prepare_response, set_cookie
58 from astakos.im.forms import *
59 from astakos.im.functions import send_greeting
60 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
61 from astakos.im.functions import invite as invite_func
63 logger = logging.getLogger(__name__)
65 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
67 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
68 keyword argument and returns an ``django.http.HttpResponse`` with the
72 tab = template.partition('_')[0].partition('.html')[0]
73 kwargs.setdefault('tab', tab)
74 html = render_to_string(template, kwargs, context_instance=context_instance)
75 response = HttpResponse(html, status=status)
77 set_cookie(response, context_instance['request'].user)
81 def requires_anonymous(func):
83 Decorator checkes whether the request.user is Anonymous and in that case
84 redirects to `logout`.
87 def wrapper(request, *args):
88 if not request.user.is_anonymous():
89 next = urlencode({'next': request.build_absolute_uri()})
90 login_uri = reverse(logout) + '?' + next
91 return HttpResponseRedirect(login_uri)
92 return func(request, *args)
95 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
97 If there is logged on user renders the profile page otherwise renders login page.
101 ``login_template_name``
102 A custom login template to use. This is optional; if not specified,
103 this will default to ``im/login.html``.
105 ``profile_template_name``
106 A custom profile template to use. This is optional; if not specified,
107 this will default to ``im/profile.html``.
110 An dictionary of variables to add to the template context.
114 im/profile.html or im/login.html or ``template_name`` keyword argument.
117 template_name = login_template_name
118 formclass = 'LoginForm'
120 if request.user.is_authenticated():
121 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
122 return render_response(template_name,
123 form = globals()[formclass](**kwargs),
124 context_instance = get_context(request, extra_context))
127 @transaction.commit_manually
128 def invite(request, template_name='im/invitations.html', extra_context={}):
130 Allows a user to invite somebody else.
132 In case of GET request renders a form for providing the invitee information.
133 In case of POST checks whether the user has not run out of invitations and then
134 sends an invitation email to singup to the service.
136 The view uses commit_manually decorator in order to ensure the number of the
137 user invitations is going to be updated only if the email has been successfully sent.
139 If the user isn't logged in, redirects to settings.LOGIN_URL.
144 A custom template to use. This is optional; if not specified,
145 this will default to ``im/invitations.html``.
148 An dictionary of variables to add to the template context.
152 im/invitations.html or ``template_name`` keyword argument.
156 The view expectes the following settings are defined:
158 * LOGIN_URL: login uri
159 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
160 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
164 inviter = AstakosUser.objects.get(username = request.user.username)
166 if request.method == 'POST':
167 username = request.POST.get('uniq')
168 realname = request.POST.get('realname')
170 if inviter.invitations > 0:
172 invite_func(inviter, username, realname)
173 status = messages.SUCCESS
174 message = _('Invitation sent to %s' % username)
176 except (SMTPException, socket.error) as e:
177 status = messages.ERROR
178 message = getattr(e, 'strerror', '')
179 transaction.rollback()
180 except IntegrityError, e:
181 status = messages.ERROR
182 message = _('There is already invitation for %s' % username)
183 transaction.rollback()
185 status = messages.ERROR
186 message = _('No invitations left')
187 messages.add_message(request, status, message)
189 sent = [{'email': inv.username,
190 'realname': inv.realname,
191 'is_consumed': inv.is_consumed}
192 for inv in inviter.invitations_sent.all()]
193 kwargs = {'inviter': inviter,
195 context = get_context(request, extra_context, **kwargs)
196 return render_response(template_name,
197 context_instance = context)
200 def edit_profile(request, template_name='im/profile.html', extra_context={}):
202 Allows a user to edit his/her profile.
204 In case of GET request renders a form for displaying the user information.
205 In case of POST updates the user informantion and redirects to ``next``
206 url parameter if exists.
208 If the user isn't logged in, redirects to settings.LOGIN_URL.
213 A custom template to use. This is optional; if not specified,
214 this will default to ``im/profile.html``.
217 An dictionary of variables to add to the template context.
221 im/profile.html or ``template_name`` keyword argument.
225 The view expectes the following settings are defined:
227 * LOGIN_URL: login uri
229 form = ProfileForm(instance=request.user)
230 extra_context['next'] = request.GET.get('next')
232 if request.method == 'POST':
233 form = ProfileForm(request.POST, instance=request.user)
236 prev_token = request.user.auth_token
238 reset_cookie = user.auth_token != prev_token
239 form = ProfileForm(instance=user)
240 next = request.POST.get('next')
242 return redirect(next)
243 msg = _('Profile has been updated successfully')
244 messages.add_message(request, messages.SUCCESS, msg)
245 except ValueError, ve:
246 messages.add_message(request, messages.ERROR, ve)
247 return render_response(template_name,
248 reset_cookie = reset_cookie,
250 context_instance = get_context(request,
253 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
255 Allows a user to create a local account.
257 In case of GET request renders a form for providing the user information.
258 In case of POST handles the signup.
260 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
261 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
262 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
265 Upon successful user creation if ``next`` url parameter is present the user is redirected there
266 otherwise renders the same page with a success message.
268 On unsuccessful creation, renders ``on_failure`` with an error message.
273 A custom template to render in case of failure. This is optional;
274 if not specified, this will default to ``im/signup.html``.
278 A custom template to render in case of success. This is optional;
279 if not specified, this will default to ``im/signup_complete.html``.
282 An dictionary of variables to add to the template context.
286 im/signup.html or ``on_failure`` keyword argument.
287 im/signup_complete.html or ``on_success`` keyword argument.
289 if request.user.is_authenticated():
290 return HttpResponseRedirect(reverse('astakos.im.views.index'))
293 backend = get_backend(request)
294 for provider in IM_MODULES:
295 extra_context['%s_form' % provider] = backend.get_signup_form(provider)
296 if request.method == 'POST':
297 provider = request.POST.get('provider')
298 next = request.POST.get('next', '')
299 form = extra_context['%s_form' % provider]
301 if provider != 'local':
302 url = reverse('astakos.im.target.%s.login' % provider)
303 url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
304 if backend.invitation:
305 url = '%s&code=%s' % (url, backend.invitation.code)
308 status, message, user = backend.signup(form)
309 if user and user.is_active:
310 return prepare_response(request, user, next=next)
311 messages.add_message(request, status, message)
312 return render_response(on_success,
313 context_instance=get_context(request, extra_context))
314 except (Invitation.DoesNotExist, ValueError), e:
315 messages.add_message(request, messages.ERROR, e)
316 for provider in IM_MODULES:
317 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
318 formclass = '%sUserCreationForm' % main
319 extra_context['%s_form' % provider] = globals()[formclass]()
320 return render_response(on_failure,
321 context_instance=get_context(request, extra_context))
324 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
326 Allows a user to send feedback.
328 In case of GET request renders a form for providing the feedback information.
329 In case of POST sends an email to support team.
331 If the user isn't logged in, redirects to settings.LOGIN_URL.
336 A custom template to use. This is optional; if not specified,
337 this will default to ``im/feedback.html``.
340 An dictionary of variables to add to the template context.
344 im/signup.html or ``template_name`` keyword argument.
348 * LOGIN_URL: login uri
349 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
351 if request.method == 'GET':
352 form = FeedbackForm()
353 if request.method == 'POST':
355 return HttpResponse('Unauthorized', status=401)
357 form = FeedbackForm(request.POST)
359 subject = _("Feedback from %s alpha2 testing" % SITENAME)
360 from_email = request.user.email
361 recipient_list = [DEFAULT_CONTACT_EMAIL]
362 content = render_to_string(email_template_name, {
363 'message': form.cleaned_data['feedback_msg'],
364 'data': form.cleaned_data['feedback_data'],
368 send_mail(subject, content, from_email, recipient_list)
369 message = _('Feedback successfully sent')
370 status = messages.SUCCESS
371 except (SMTPException, socket.error) as e:
372 status = messages.ERROR
373 message = getattr(e, 'strerror', '')
374 messages.add_message(request, status, message)
375 return render_response(template_name,
377 context_instance = get_context(request, extra_context))
379 def logout(request, template='registration/logged_out.html', extra_context={}):
381 Wraps `django.contrib.auth.logout` and delete the cookie.
384 response = HttpResponse()
385 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
386 next = request.GET.get('next')
388 response['Location'] = next
389 response.status_code = 302
392 response['Location'] = LOGOUT_NEXT
393 response.status_code = 301
395 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
396 context = get_context(request, extra_context)
397 response.write(render_to_string(template, context_instance=context))
400 @transaction.commit_manually
401 def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
403 Activates the user identified by the ``auth`` request parameter, sends a welcome email
404 and renews the user token.
406 The view uses commit_manually decorator in order to ensure the user state will be updated
407 only if the email will be send successfully.
409 token = request.GET.get('auth')
410 next = request.GET.get('next')
412 user = AstakosUser.objects.get(auth_token=token)
413 except AstakosUser.DoesNotExist:
414 return HttpResponseBadRequest(_('No such user'))
416 user.is_active = True
417 user.email_verified = True
420 send_greeting(user, email_template_name)
421 response = prepare_response(request, user, next, renew=True)
424 except (SMTPException, socket.error) as e:
425 message = getattr(e, 'name') if hasattr(e, 'name') else e
426 messages.add_message(request, messages.ERROR, message)
427 transaction.rollback()
428 return signup(request, on_failure='im/signup.html')