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
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.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
60 from astakos.im.functions import invite as invite_func
62 logger = logging.getLogger(__name__)
64 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
66 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
67 keyword argument and returns an ``django.http.HttpResponse`` with the
71 tab = template.partition('_')[0].partition('.html')[0]
72 kwargs.setdefault('tab', tab)
73 html = render_to_string(template, kwargs, context_instance=context_instance)
74 response = HttpResponse(html, status=status)
76 set_cookie(response, context_instance['request'].user)
80 def requires_anonymous(func):
82 Decorator checkes whether the request.user is Anonymous and in that case
83 redirects to `logout`.
86 def wrapper(request, *args):
87 if not request.user.is_anonymous():
88 next = urlencode({'next': request.build_absolute_uri()})
89 login_uri = reverse(logout) + '?' + next
90 return HttpResponseRedirect(login_uri)
91 return func(request, *args)
94 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
96 If there is logged on user renders the profile page otherwise renders login page.
100 ``login_template_name``
101 A custom login template to use. This is optional; if not specified,
102 this will default to ``im/login.html``.
104 ``profile_template_name``
105 A custom profile template to use. This is optional; if not specified,
106 this will default to ``im/profile.html``.
109 An dictionary of variables to add to the template context.
113 im/profile.html or im/login.html or ``template_name`` keyword argument.
116 template_name = login_template_name
117 formclass = 'LoginForm'
119 if request.user.is_authenticated():
120 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
121 return render_response(template_name,
122 form = globals()[formclass](**kwargs),
123 context_instance = get_context(request, extra_context))
126 @transaction.commit_manually
127 def invite(request, template_name='im/invitations.html', extra_context={}):
129 Allows a user to invite somebody else.
131 In case of GET request renders a form for providing the invitee information.
132 In case of POST checks whether the user has not run out of invitations and then
133 sends an invitation email to singup to the service.
135 The view uses commit_manually decorator in order to ensure the number of the
136 user invitations is going to be updated only if the email has been successfully sent.
138 If the user isn't logged in, redirects to settings.LOGIN_URL.
143 A custom template to use. This is optional; if not specified,
144 this will default to ``im/invitations.html``.
147 An dictionary of variables to add to the template context.
151 im/invitations.html or ``template_name`` keyword argument.
155 The view expectes the following settings are defined:
157 * LOGIN_URL: login uri
158 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
159 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
163 inviter = AstakosUser.objects.get(username = request.user.username)
165 if request.method == 'POST':
166 username = request.POST.get('uniq')
167 realname = request.POST.get('realname')
169 if inviter.invitations > 0:
171 invite_func(inviter, username, realname)
172 status = messages.SUCCESS
173 message = _('Invitation sent to %s' % username)
175 except (SMTPException, socket.error) as e:
176 status = messages.ERROR
177 message = getattr(e, 'strerror', '')
178 transaction.rollback()
179 except IntegrityError, e:
180 status = messages.ERROR
181 message = _('There is already invitation for %s' % username)
182 transaction.rollback()
184 status = messages.ERROR
185 message = _('No invitations left')
186 messages.add_message(request, status, message)
188 sent = [{'email': inv.username,
189 'realname': inv.realname,
190 'is_consumed': inv.is_consumed}
191 for inv in inviter.invitations_sent.all()]
192 kwargs = {'inviter': inviter,
194 context = get_context(request, extra_context, **kwargs)
195 return render_response(template_name,
196 context_instance = context)
199 def edit_profile(request, template_name='im/profile.html', extra_context={}):
201 Allows a user to edit his/her profile.
203 In case of GET request renders a form for displaying the user information.
204 In case of POST updates the user informantion and redirects to ``next``
205 url parameter if exists.
207 If the user isn't logged in, redirects to settings.LOGIN_URL.
212 A custom template to use. This is optional; if not specified,
213 this will default to ``im/profile.html``.
216 An dictionary of variables to add to the template context.
220 im/profile.html or ``template_name`` keyword argument.
224 The view expectes the following settings are defined:
226 * LOGIN_URL: login uri
228 form = ProfileForm(instance=request.user)
229 extra_context['next'] = request.GET.get('next')
231 if request.method == 'POST':
232 form = ProfileForm(request.POST, instance=request.user)
235 prev_token = request.user.auth_token
237 reset_cookie = user.auth_token != prev_token
238 form = ProfileForm(instance=user)
239 next = request.POST.get('next')
241 return redirect(next)
242 msg = _('Profile has been updated successfully')
243 messages.add_message(request, messages.SUCCESS, msg)
244 except ValueError, ve:
245 messages.add_message(request, messages.ERROR, ve)
246 return render_response(template_name,
247 reset_cookie = reset_cookie,
249 context_instance = get_context(request,
252 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
254 Allows a user to create a local account.
256 In case of GET request renders a form for providing the user information.
257 In case of POST handles the signup.
259 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
260 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
261 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
264 Upon successful user creation if ``next`` url parameter is present the user is redirected there
265 otherwise renders the same page with a success message.
267 On unsuccessful creation, renders ``on_failure`` with an error message.
272 A custom template to render in case of failure. This is optional;
273 if not specified, this will default to ``im/signup.html``.
277 A custom template to render in case of success. This is optional;
278 if not specified, this will default to ``im/signup_complete.html``.
281 An dictionary of variables to add to the template context.
285 im/signup.html or ``on_failure`` keyword argument.
286 im/signup_complete.html or ``on_success`` keyword argument.
288 if request.user.is_authenticated():
289 return HttpResponseRedirect(reverse('astakos.im.views.index'))
292 backend = get_backend(request)
293 for provider in IM_MODULES:
294 extra_context['%s_form' % provider] = backend.get_signup_form(provider)
295 if request.method == 'POST':
296 provider = request.POST.get('provider')
297 next = request.POST.get('next', '')
298 form = extra_context['%s_form' % provider]
300 if provider != 'local':
301 url = reverse('astakos.im.target.%s.login' % provider)
302 url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
303 if backend.invitation:
304 url = '%s&code=%s' % (url, backend.invitation.code)
307 status, message, user = backend.signup(form)
308 if user and user.is_active:
309 return prepare_response(request, user, next=next)
310 messages.add_message(request, status, message)
311 return render_response(on_success,
312 context_instance=get_context(request, extra_context))
313 except (Invitation.DoesNotExist, ValueError), e:
314 messages.add_message(request, messages.ERROR, e)
315 for provider in IM_MODULES:
316 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
317 formclass = '%sUserCreationForm' % main
318 extra_context['%s_form' % provider] = globals()[formclass]()
319 return render_response(on_failure,
320 context_instance=get_context(request, extra_context))
323 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
325 Allows a user to send feedback.
327 In case of GET request renders a form for providing the feedback information.
328 In case of POST sends an email to support team.
330 If the user isn't logged in, redirects to settings.LOGIN_URL.
335 A custom template to use. This is optional; if not specified,
336 this will default to ``im/feedback.html``.
339 An dictionary of variables to add to the template context.
343 im/signup.html or ``template_name`` keyword argument.
347 * LOGIN_URL: login uri
348 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
350 if request.method == 'GET':
351 form = FeedbackForm()
352 if request.method == 'POST':
354 return HttpResponse('Unauthorized', status=401)
356 form = FeedbackForm(request.POST)
358 subject = _("Feedback from %s alpha2 testing" % SITENAME)
359 from_email = request.user.email
360 recipient_list = [DEFAULT_CONTACT_EMAIL]
361 content = render_to_string(email_template_name, {
362 'message': form.cleaned_data['feedback_msg'],
363 'data': form.cleaned_data['feedback_data'],
367 send_mail(subject, content, from_email, recipient_list)
368 message = _('Feedback successfully sent')
369 status = messages.SUCCESS
370 except (SMTPException, socket.error) as e:
371 status = messages.ERROR
372 message = getattr(e, 'strerror', '')
373 messages.add_message(request, status, message)
374 return render_response(template_name,
376 context_instance = get_context(request, extra_context))
378 def logout(request, template='registration/logged_out.html', extra_context={}):
380 Wraps `django.contrib.auth.logout` and delete the cookie.
383 response = HttpResponse()
384 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
385 next = request.GET.get('next')
387 response['Location'] = next
388 response.status_code = 302
391 response['Location'] = LOGOUT_NEXT
392 response.status_code = 301
394 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
395 context = get_context(request, extra_context)
396 response.write(render_to_string(template, context_instance=context))
399 def activate(request):
401 Activates the user identified by the ``auth`` request parameter
403 token = request.GET.get('auth')
404 next = request.GET.get('next')
406 user = AstakosUser.objects.get(auth_token=token)
407 except AstakosUser.DoesNotExist:
408 return HttpResponseBadRequest('No such user')
410 user.is_active = True
411 user.email_verified = True
413 return prepare_response(request, user, next, renew=True)