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 random import randint
38 from smtplib import SMTPException
39 from urllib import quote
40 from functools import wraps
42 from django.conf import settings
43 from django.core.mail import send_mail
44 from django.http import HttpResponse
45 from django.shortcuts import redirect
46 from django.template.loader import render_to_string
47 from django.utils.translation import ugettext as _
48 from django.core.urlresolvers import reverse
49 from django.contrib.auth.decorators import login_required
50 from django.contrib import messages
51 from django.db import transaction
52 from django.contrib.auth.views import logout
53 from django.utils.http import urlencode
54 from django.http import HttpResponseRedirect
56 from astakos.im.models import AstakosUser, Invitation
57 from astakos.im.backends import get_backend
58 from astakos.im.util import get_context, get_current_site, prepare_response
59 from astakos.im.forms import *
61 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
63 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
64 keyword argument and returns an ``django.http.HttpResponse`` with the
68 tab = template.partition('_')[0].partition('.html')[0]
69 kwargs.setdefault('tab', tab)
70 html = render_to_string(template, kwargs, context_instance=context_instance)
71 return HttpResponse(html, status=status)
74 def requires_anonymous(func):
76 Decorator checkes whether the request.user is Anonymous and in that case
77 redirects to `user_logout`.
80 def wrapper(request, *args):
81 if not request.user.is_anonymous():
82 next = urlencode({'next': request.build_absolute_uri()})
83 login_uri = reverse(user_logout) + '?' + next
84 return HttpResponseRedirect(login_uri)
85 return func(request, *args)
88 def index(request, login_template_name='login.html', profile_template_name='profile.html', extra_context={}):
90 If there is logged on user renders the profile page otherwise renders login page.
94 ``login_template_name``
95 A custom login template to use. This is optional; if not specified,
96 this will default to ``login.html``.
98 ``profile_template_name``
99 A custom profile template to use. This is optional; if not specified,
100 this will default to ``profile.html``.
103 An dictionary of variables to add to the template context.
107 profile.html or login.html or ``template_name`` keyword argument.
110 template_name = login_template_name
111 formclass = 'LoginForm'
113 if request.user.is_authenticated():
114 template_name = profile_template_name
115 formclass = 'ProfileForm'
116 kwargs.update({'instance':request.user})
117 return render_response(template_name,
118 form = globals()[formclass](**kwargs),
119 context_instance = get_context(request, extra_context))
121 def _generate_invitation_code():
123 code = randint(1, 2L**63 - 1)
125 Invitation.objects.get(code=code)
126 # An invitation with this code already exists, try again
127 except Invitation.DoesNotExist:
130 def _send_invitation(request, baseurl, inv):
131 sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
132 subject = _('Invitation to %s' % sitename)
133 url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(sitedomain))
134 message = render_to_string('invitation.txt', {
139 'support': settings.DEFAULT_CONTACT_EMAIL % sitename.lower()})
140 sender = settings.DEFAULT_FROM_EMAIL % sitename
141 send_mail(subject, message, sender, [inv.username])
142 logging.info('Sent invitation %s', inv)
145 @transaction.commit_manually
146 def invite(request, template_name='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 ``invitations.html``.
166 An dictionary of variables to add to the template context.
170 invitations.html or ``template_name`` keyword argument.
174 The view expectes the following settings are defined:
176 * LOGIN_URL: login uri
177 * SIGNUP_TARGET: Where users should signup with their invitation code
178 * DEFAULT_CONTACT_EMAIL: service support email
179 * DEFAULT_FROM_EMAIL: from email
183 inviter = AstakosUser.objects.get(username = request.user.username)
185 if request.method == 'POST':
186 username = request.POST.get('uniq')
187 realname = request.POST.get('realname')
189 if inviter.invitations > 0:
190 code = _generate_invitation_code()
191 invitation, created = Invitation.objects.get_or_create(
194 defaults={'code': code, 'realname': realname})
197 baseurl = request.build_absolute_uri('/').rstrip('/')
198 _send_invitation(request, baseurl, invitation)
200 inviter.invitations = max(0, inviter.invitations - 1)
202 status = messages.SUCCESS
203 message = _('Invitation sent to %s' % username)
205 except (SMTPException, socket.error) as e:
206 status = messages.ERROR
207 message = getattr(e, 'strerror', '')
208 transaction.rollback()
210 status = messages.ERROR
211 message = _('No invitations left')
212 messages.add_message(request, status, message)
214 sent = [{'email': inv.username,
215 'realname': inv.realname,
216 'is_accepted': inv.is_accepted}
217 for inv in inviter.invitations_sent.all()]
218 kwargs = {'user': inviter,
220 context = get_context(request, extra_context, **kwargs)
221 return render_response(template_name,
222 context_instance = context)
225 def edit_profile(request, template_name='profile.html', extra_context={}):
227 Allows a user to edit his/her profile.
229 In case of GET request renders a form for displaying the user information.
230 In case of POST updates the user informantion and redirects to ``next``
231 url parameter if exists.
233 If the user isn't logged in, redirects to settings.LOGIN_URL.
238 A custom template to use. This is optional; if not specified,
239 this will default to ``profile.html``.
242 An dictionary of variables to add to the template context.
246 profile.html or ``template_name`` keyword argument.
248 form = ProfileForm(instance=request.user)
249 extra_context['next'] = request.GET.get('next')
250 if request.method == 'POST':
251 form = ProfileForm(request.POST, instance=request.user)
255 msg = _('Profile has been updated successfully')
256 messages.add_message(request, messages.SUCCESS, msg)
257 except ValueError, ve:
258 messages.add_message(request, messages.ERROR, ve)
259 next = request.POST.get('next')
261 return redirect(next)
262 return render_response(template_name,
264 context_instance = get_context(request,
268 def signup(request, template_name='signup.html', extra_context={}, backend=None):
270 Allows a user to create a local account.
272 In case of GET request renders a form for providing the user information.
273 In case of POST handles the signup.
275 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
276 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
277 if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
280 Upon successful user creation if ``next`` url parameter is present the user is redirected there
281 otherwise renders the same page with a success message.
283 On unsuccessful creation, renders the same page with an error message.
288 A custom template to use. This is optional; if not specified,
289 this will default to ``signup.html``.
292 An dictionary of variables to add to the template context.
296 signup.html or ``template_name`` keyword argument.
300 backend = get_backend(request)
301 for provider in settings.IM_MODULES:
302 extra_context['%s_form' % provider] = backend.get_signup_form(provider)
303 if request.method == 'POST':
304 provider = request.POST.get('provider')
305 next = request.POST.get('next', '')
306 form = extra_context['%s_form' % provider]
308 if provider != 'local':
309 url = reverse('astakos.im.target.%s.login' % provider)
310 url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
311 if backend.invitation:
312 url = '%s&code=%s' % (url, backend.invitation.code)
315 status, message, user = backend.signup(form)
316 if user and user.is_active:
317 return prepare_response(request, user, next=next)
318 messages.add_message(request, status, message)
319 except (Invitation.DoesNotExist, ValueError), e:
320 messages.add_message(request, messages.ERROR, e)
321 for provider in settings.IM_MODULES:
322 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
323 formclass = '%sUserCreationForm' % main
324 extra_context['%s_form' % provider] = globals()[formclass]()
325 return render_response(template_name,
326 context_instance=get_context(request, extra_context))
329 def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
331 Allows a user to send feedback.
333 In case of GET request renders a form for providing the feedback information.
334 In case of POST sends an email to support team.
336 If the user isn't logged in, redirects to settings.LOGIN_URL.
341 A custom template to use. This is optional; if not specified,
342 this will default to ``feedback.html``.
345 An dictionary of variables to add to the template context.
349 signup.html or ``template_name`` keyword argument.
353 * DEFAULT_CONTACT_EMAIL: List of feedback recipients
355 if request.method == 'GET':
356 form = FeedbackForm()
357 if request.method == 'POST':
359 return HttpResponse('Unauthorized', status=401)
361 form = FeedbackForm(request.POST)
363 sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
364 subject = _("Feedback from %s" % sitename)
365 from_email = request.user.email
366 recipient_list = [settings.DEFAULT_CONTACT_EMAIL % sitename.lower()]
367 content = render_to_string(email_template_name, {
368 'message': form.cleaned_data['feedback_msg'],
369 'data': form.cleaned_data['feedback_data'],
373 send_mail(subject, content, from_email, recipient_list)
374 message = _('Feedback successfully sent')
375 status = messages.SUCCESS
376 except (SMTPException, socket.error) as e:
377 status = messages.ERROR
378 message = getattr(e, 'strerror', '')
379 messages.add_message(request, status, message)
380 return render_response(template_name,
382 context_instance = get_context(request, extra_context))
384 def create_user(request, form, backend=None, post_data={}, next = None, template_name='login.html', extra_context={}):
387 backend = get_backend(request)
389 status, message, user = backend.signup(form)
390 if status == messages.SUCCESS:
391 for k,v in post_data.items():
395 return prepare_response(request, user, next=next)
396 messages.add_message(request, status, message)
398 messages.add_message(request, messages.ERROR, form.errors)
399 except (Invitation.DoesNotExist, ValueError), e:
400 messages.add_message(request, messages.ERROR, e)
401 return render_response(template_name,
402 form = LocalUserCreationForm(),
403 context_instance=get_context(request, extra_context))
405 def user_logout(request):
407 Wraps `django.contrib.auth.views.logout` and delete the cookie.
409 response = logout(request)
410 response.delete_cookie(settings.COOKIE_NAME)
411 next = request.GET.get('next')
413 response['Location'] = next
414 response.status_code = 302