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
54 from astakos.im.models import AstakosUser, Invitation
55 from astakos.im.backends import get_backend
56 from astakos.im.util import get_context, prepare_response, set_cookie
57 from astakos.im.forms import *
58 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
59 from astakos.im.functions import invite as invite_func
61 logger = logging.getLogger(__name__)
63 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
65 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
66 keyword argument and returns an ``django.http.HttpResponse`` with the
70 tab = template.partition('_')[0].partition('.html')[0]
71 kwargs.setdefault('tab', tab)
72 html = render_to_string(template, kwargs, context_instance=context_instance)
73 response = HttpResponse(html, status=status)
75 set_cookie(response, context_instance['request'].user)
79 def requires_anonymous(func):
81 Decorator checkes whether the request.user is Anonymous and in that case
82 redirects to `logout`.
85 def wrapper(request, *args):
86 if not request.user.is_anonymous():
87 next = urlencode({'next': request.build_absolute_uri()})
88 login_uri = reverse(logout) + '?' + next
89 return HttpResponseRedirect(login_uri)
90 return func(request, *args)
93 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
95 If there is logged on user renders the profile page otherwise renders login page.
99 ``login_template_name``
100 A custom login template to use. This is optional; if not specified,
101 this will default to ``im/login.html``.
103 ``profile_template_name``
104 A custom profile template to use. This is optional; if not specified,
105 this will default to ``im/profile.html``.
108 An dictionary of variables to add to the template context.
112 im/profile.html or im/login.html or ``template_name`` keyword argument.
115 template_name = login_template_name
116 formclass = 'LoginForm'
118 if request.user.is_authenticated():
119 template_name = profile_template_name
120 formclass = 'ProfileForm'
121 kwargs.update({'instance':request.user})
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()
181 status = messages.ERROR
182 message = _('No invitations left')
183 messages.add_message(request, status, message)
185 sent = [{'email': inv.username,
186 'realname': inv.realname,
187 'is_consumed': inv.is_consumed}
188 for inv in inviter.invitations_sent.all()]
189 kwargs = {'inviter': inviter,
191 context = get_context(request, extra_context, **kwargs)
192 return render_response(template_name,
193 context_instance = context)
196 def edit_profile(request, template_name='im/profile.html', extra_context={}):
198 Allows a user to edit his/her profile.
200 In case of GET request renders a form for displaying the user information.
201 In case of POST updates the user informantion and redirects to ``next``
202 url parameter if exists.
204 If the user isn't logged in, redirects to settings.LOGIN_URL.
209 A custom template to use. This is optional; if not specified,
210 this will default to ``im/profile.html``.
213 An dictionary of variables to add to the template context.
217 im/profile.html or ``template_name`` keyword argument.
221 The view expectes the following settings are defined:
223 * LOGIN_URL: login uri
225 form = ProfileForm(instance=request.user)
226 extra_context['next'] = request.GET.get('next')
228 if request.method == 'POST':
229 form = ProfileForm(request.POST, instance=request.user)
232 prev_token = request.user.auth_token
234 reset_cookie = user.auth_token != prev_token
235 form = ProfileForm(instance=user)
236 next = request.POST.get('next')
238 return redirect(next)
239 msg = _('Profile has been updated successfully')
240 messages.add_message(request, messages.SUCCESS, msg)
241 except ValueError, ve:
242 messages.add_message(request, messages.ERROR, ve)
243 return render_response(template_name,
244 reset_cookie = reset_cookie,
246 context_instance = get_context(request,
249 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
251 Allows a user to create a local account.
253 In case of GET request renders a form for providing the user information.
254 In case of POST handles the signup.
256 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
257 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
258 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
261 Upon successful user creation if ``next`` url parameter is present the user is redirected there
262 otherwise renders the same page with a success message.
264 On unsuccessful creation, renders the same page with an error message.
269 A custom template to render in case of failure. This is optional;
270 if not specified, this will default to ``im/signup.html``.
274 A custom template to render in case of success. This is optional;
275 if not specified, this will default to ``im/signup_complete.html``.
278 An dictionary of variables to add to the template context.
282 im/signup.html or ``on_failure`` keyword argument.
283 im/signup_complete.html or ``on_success`` keyword argument.
285 if request.user.is_authenticated():
286 return HttpResponseRedirect(reverse('astakos.im.views.index'))
289 backend = get_backend(request)
290 for provider in IM_MODULES:
291 extra_context['%s_form' % provider] = backend.get_signup_form(provider)
292 if request.method == 'POST':
293 provider = request.POST.get('provider')
294 next = request.POST.get('next', '')
295 form = extra_context['%s_form' % provider]
297 if provider != 'local':
298 url = reverse('astakos.im.target.%s.login' % provider)
299 url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
300 if backend.invitation:
301 url = '%s&code=%s' % (url, backend.invitation.code)
304 status, message, user = backend.signup(form)
305 if user and user.is_active:
306 return prepare_response(request, user, next=next)
307 messages.add_message(request, status, message)
308 return render_response(on_success,
309 context_instance=get_context(request, extra_context))
310 except (Invitation.DoesNotExist, ValueError), e:
311 messages.add_message(request, messages.ERROR, e)
312 for provider in IM_MODULES:
313 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
314 formclass = '%sUserCreationForm' % main
315 extra_context['%s_form' % provider] = globals()[formclass]()
316 return render_response(on_failure,
317 context_instance=get_context(request, extra_context))
320 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
322 Allows a user to send feedback.
324 In case of GET request renders a form for providing the feedback information.
325 In case of POST sends an email to support team.
327 If the user isn't logged in, redirects to settings.LOGIN_URL.
332 A custom template to use. This is optional; if not specified,
333 this will default to ``im/feedback.html``.
336 An dictionary of variables to add to the template context.
340 im/signup.html or ``template_name`` keyword argument.
344 * LOGIN_URL: login uri
345 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
347 if request.method == 'GET':
348 form = FeedbackForm()
349 if request.method == 'POST':
351 return HttpResponse('Unauthorized', status=401)
353 form = FeedbackForm(request.POST)
355 subject = _("Feedback from %s" % SITENAME)
356 from_email = request.user.email
357 recipient_list = [DEFAULT_CONTACT_EMAIL]
358 content = render_to_string(email_template_name, {
359 'message': form.cleaned_data['feedback_msg'],
360 'data': form.cleaned_data['feedback_data'],
364 send_mail(subject, content, from_email, recipient_list)
365 message = _('Feedback successfully sent')
366 status = messages.SUCCESS
367 except (SMTPException, socket.error) as e:
368 status = messages.ERROR
369 message = getattr(e, 'strerror', '')
370 messages.add_message(request, status, message)
371 return render_response(template_name,
373 context_instance = get_context(request, extra_context))
375 def logout(request, template='registration/logged_out.html', extra_context={}):
377 Wraps `django.contrib.auth.logout` and delete the cookie.
380 response = HttpResponse()
381 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
382 next = request.GET.get('next')
384 response['Location'] = next
385 response.status_code = 302
388 response['Location'] = LOGOUT_NEXT
389 response.status_code = 301
391 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
392 context = get_context(request, extra_context)
393 response.write(render_to_string(template, context_instance=context))
396 def activate(request):
398 Activates the user identified by the ``auth`` request parameter
400 token = request.GET.get('auth')
401 next = request.GET.get('next')
403 user = AstakosUser.objects.get(auth_token=token)
404 except AstakosUser.DoesNotExist:
405 return HttpResponseBadRequest('No such user')
407 user.is_active = True
409 return prepare_response(request, user, next, renew=True)