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.
40 from datetime import datetime
41 from functools import wraps
43 from random import randint
44 from smtplib import SMTPException
45 from hashlib import new as newhasher
46 from urllib import quote
48 from django.conf import settings
49 from django.core.mail import send_mail
50 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
51 from django.shortcuts import redirect
52 from django.template.loader import render_to_string
53 from django.shortcuts import render_to_response
54 from django.utils.http import urlencode
55 from django.utils.translation import ugettext as _
56 from django.core.urlresolvers import reverse
57 from django.contrib.auth.models import AnonymousUser
58 from django.contrib.auth.decorators import login_required
59 from django.contrib.sites.models import Site
60 from django.contrib import messages
61 from django.db import transaction
62 from django.contrib.auth.forms import UserCreationForm
64 #from astakos.im.openid_store import PithosOpenIDStore
65 from astakos.im.models import AstakosUser, Invitation
66 from astakos.im.util import isoformat, get_context
67 from astakos.im.backends import get_backend
68 from astakos.im.forms import ProfileForm, FeedbackForm, LoginForm
70 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
72 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
73 keyword argument and returns an ``django.http.HttpResponse`` with the
77 tab = template.partition('_')[0]
78 kwargs.setdefault('tab', tab)
79 html = render_to_string(template, kwargs, context_instance=context_instance)
80 return HttpResponse(html, status=status)
82 def index(request, login_template_name='login.html', profile_template_name='profile.html', extra_context={}):
84 If there is logged on user renders the profile page otherwise renders login page.
88 ``login_template_name``
89 A custom login template to use. This is optional; if not specified,
90 this will default to ``login.html``.
92 ``profile_template_name``
93 A custom profile template to use. This is optional; if not specified,
94 this will default to ``login.html``.
97 An dictionary of variables to add to the template context.
101 index.html or ``template_name`` keyword argument.
104 template_name = login_template_name
105 formclass = 'LoginForm'
107 if request.user.is_authenticated():
108 template_name = profile_template_name
109 formclass = 'ProfileForm'
110 kwargs.update({'instance':request.user})
111 return render_response(template_name,
112 form = globals()[formclass](**kwargs),
113 context_instance = get_context(request, extra_context))
115 def _generate_invitation_code():
117 code = randint(1, 2L**63 - 1)
119 Invitation.objects.get(code=code)
120 # An invitation with this code already exists, try again
121 except Invitation.DoesNotExist:
124 def _send_invitation(request, baseurl, inv):
125 site = Site.objects.get_current()
126 subject = _('Invitation to %s' % site.name)
127 url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(site.domain))
128 message = render_to_string('invitation.txt', {
132 'service': site.name,
133 'support': settings.DEFAULT_CONTACT_EMAIL % site.name.lower()})
134 sender = settings.DEFAULT_FROM_EMAIL % site.name
135 send_mail(subject, message, sender, [inv.username])
136 logging.info('Sent invitation %s', inv)
139 @transaction.commit_manually
140 def invite(request, template_name='invitations.html', extra_context={}):
142 Allows a user to invite somebody else.
144 In case of GET request renders a form for providing the invitee information.
145 In case of POST checks whether the user has not run out of invitations and then
146 sends an invitation email to singup to the service.
148 The view uses commit_manually decorator in order to ensure the number of the
149 user invitations is going to be updated only if the email has been successfully sent.
151 If the user isn't logged in, redirects to settings.LOGIN_URL.
156 A custom template to use. This is optional; if not specified,
157 this will default to ``invitations.html``.
160 An dictionary of variables to add to the template context.
164 invitations.html or ``template_name`` keyword argument.
168 The view expectes the following settings are defined:
170 * LOGIN_URL: login uri
171 * SIGNUP_TARGET: Where users should signup with their invitation code
172 * DEFAULT_CONTACT_EMAIL: service support email
173 * DEFAULT_FROM_EMAIL: from email
177 inviter = AstakosUser.objects.get(username = request.user.username)
179 if request.method == 'POST':
180 username = request.POST.get('uniq')
181 realname = request.POST.get('realname')
183 if inviter.invitations > 0:
184 code = _generate_invitation_code()
185 invitation, created = Invitation.objects.get_or_create(
188 defaults={'code': code, 'realname': realname})
191 baseurl = request.build_absolute_uri('/').rstrip('/')
192 _send_invitation(request, baseurl, invitation)
194 inviter.invitations = max(0, inviter.invitations - 1)
196 status = messages.SUCCESS
197 message = _('Invitation sent to %s' % username)
199 except (SMTPException, socket.error) as e:
200 status = messages.ERROR
201 message = getattr(e, 'strerror', '')
202 transaction.rollback()
204 status = messages.ERROR
205 message = _('No invitations left')
206 messages.add_message(request, status, message)
208 if request.GET.get('format') == 'json':
209 sent = [{'email': inv.username,
210 'realname': inv.realname,
211 'is_accepted': inv.is_accepted}
212 for inv in inviter.invitations_sent.all()]
213 rep = {'invitations': inviter.invitations, 'sent': sent}
214 return HttpResponse(json.dumps(rep))
216 kwargs = {'user': inviter}
217 context = get_context(request, extra_context, **kwargs)
218 return render_response(template_name,
219 context_instance = context)
222 def edit_profile(request, template_name='profile.html', extra_context={}):
224 Allows a user to edit his/her profile.
226 In case of GET request renders a form for displaying the user information.
227 In case of POST updates the user informantion.
229 If the user isn't logged in, redirects to settings.LOGIN_URL.
234 A custom template to use. This is optional; if not specified,
235 this will default to ``profile.html``.
238 An dictionary of variables to add to the template context.
242 profile.html or ``template_name`` keyword argument.
245 user = AstakosUser.objects.get(username=request.user)
246 form = ProfileForm(instance=user)
247 except AstakosUser.DoesNotExist:
248 token = request.GET.get('auth', None)
249 user = AstakosUser.objects.get(auth_token=token)
250 if request.method == 'POST':
251 form = ProfileForm(request.POST, instance=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 return render_response(template_name,
261 context_instance = get_context(request,
265 @transaction.commit_manually
266 def signup(request, template_name='signup.html', extra_context={}, backend=None):
268 Allows a user to create a local account.
270 In case of GET request renders a form for providing the user information.
271 In case of POST handles the signup.
273 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
274 if present, otherwise to the ``astakos.im.backends.InvitationBackend``
275 if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
278 Upon successful user creation if ``next`` url parameter is present the user is redirected there
279 otherwise renders the same page with a success message.
281 On unsuccessful creation, renders the same page with an error message.
283 The view uses commit_manually decorator in order to ensure the user will be created
284 only if the procedure has been completed successfully.
289 A custom template to use. This is optional; if not specified,
290 this will default to ``signup.html``.
293 An dictionary of variables to add to the template context.
297 signup.html or ``template_name`` keyword argument.
301 backend = get_backend(request)
302 form = backend.get_signup_form()
303 if request.method == 'POST':
305 status, message = backend.signup(form)
306 # rollback in case of error
307 if status == messages.ERROR:
308 transaction.rollback()
311 next = request.POST.get('next')
313 return redirect(next)
314 messages.add_message(request, status, message)
315 except Invitation.DoesNotExist, e:
316 messages.add_message(request, messages.ERROR, e)
317 return render_response(template_name,
318 form = form if 'form' in locals() else UserCreationForm(),
319 context_instance=get_context(request, extra_context))
322 def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
324 Allows a user to send feedback.
326 In case of GET request renders a form for providing the feedback information.
327 In case of POST sends an email to support team.
329 If the user isn't logged in, redirects to settings.LOGIN_URL.
334 A custom template to use. This is optional; if not specified,
335 this will default to ``feedback.html``.
338 An dictionary of variables to add to the template context.
342 signup.html or ``template_name`` keyword argument.
346 * DEFAULT_CONTACT_EMAIL: List of feedback recipients
348 if request.method == 'GET':
349 form = FeedbackForm()
350 if request.method == 'POST':
352 return HttpResponse('Unauthorized', status=401)
354 form = FeedbackForm(request.POST)
356 site = Site.objects.get_current()
357 subject = _("Feedback from %s" % site.name)
358 from_email = request.user.email
359 recipient_list = [settings.DEFAULT_CONTACT_EMAIL]
360 content = render_to_string(email_template_name, {
361 'message': form.cleaned_data('feedback_msg'),
362 'data': form.cleaned_data('feedback_data'),
365 send_mail(subject, content, from_email, recipient_list)
367 resp = json.dumps({'status': 'send'})
368 return HttpResponse(resp)
369 return render_response(template_name,
371 context_instance = get_context(request, extra_context))