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
54 from django.contrib.auth.views import password_change
56 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
57 from astakos.im.activation_backends import get_backend
58 from astakos.im.util import get_context, prepare_response, set_cookie, has_signed_terms
59 from astakos.im.forms import *
60 from astakos.im.functions import send_greeting, send_feedback, SendMailError
61 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
62 from astakos.im.functions import invite as invite_func
64 logger = logging.getLogger(__name__)
66 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
68 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
69 keyword argument and returns an ``django.http.HttpResponse`` with the
73 tab = template.partition('_')[0].partition('.html')[0]
74 kwargs.setdefault('tab', tab)
75 html = render_to_string(template, kwargs, context_instance=context_instance)
76 response = HttpResponse(html, status=status)
78 set_cookie(response, context_instance['request'].user)
82 def requires_anonymous(func):
84 Decorator checkes whether the request.user is not Anonymous and in that case
85 redirects to `logout`.
88 def wrapper(request, *args):
89 if not request.user.is_anonymous():
90 next = urlencode({'next': request.build_absolute_uri()})
91 logout_uri = reverse(logout) + '?' + next
92 return HttpResponseRedirect(logout_uri)
93 return func(request, *args)
96 def signed_terms_required(func):
98 Decorator checkes whether the request.user is Anonymous and in that case
99 redirects to `logout`.
102 def wrapper(request, *args, **kwargs):
103 if request.user.is_authenticated() and not has_signed_terms(request.user):
104 params = urlencode({'next': request.build_absolute_uri(),
106 terms_uri = reverse('latest_terms') + '?' + params
107 return HttpResponseRedirect(terms_uri)
108 return func(request, *args, **kwargs)
111 @signed_terms_required
112 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
114 If there is logged on user renders the profile page otherwise renders login page.
118 ``login_template_name``
119 A custom login template to use. This is optional; if not specified,
120 this will default to ``im/login.html``.
122 ``profile_template_name``
123 A custom profile template to use. This is optional; if not specified,
124 this will default to ``im/profile.html``.
127 An dictionary of variables to add to the template context.
131 im/profile.html or im/login.html or ``template_name`` keyword argument.
134 template_name = login_template_name
135 if request.user.is_authenticated():
136 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
137 return render_response(template_name,
138 login_form = LoginForm(),
139 context_instance = get_context(request, extra_context))
142 @signed_terms_required
143 @transaction.commit_manually
144 def invite(request, template_name='im/invitations.html', extra_context={}):
146 Allows a user to invite somebody else.
148 In case of GET request renders a form for providing the invitee information.
149 In case of POST checks whether the user has not run out of invitations and then
150 sends an invitation email to singup to the service.
152 The view uses commit_manually decorator in order to ensure the number of the
153 user invitations is going to be updated only if the email has been successfully sent.
155 If the user isn't logged in, redirects to settings.LOGIN_URL.
160 A custom template to use. This is optional; if not specified,
161 this will default to ``im/invitations.html``.
164 An dictionary of variables to add to the template context.
168 im/invitations.html or ``template_name`` keyword argument.
172 The view expectes the following settings are defined:
174 * LOGIN_URL: login uri
175 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
176 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
180 inviter = AstakosUser.objects.get(username = request.user.username)
181 form = InvitationForm()
183 if request.method == 'POST':
184 form = InvitationForm(request.POST)
186 if inviter.invitations > 0:
189 invitation = form.save()
190 invitation.inviter = inviter
191 invite_func(invitation, inviter)
192 status = messages.SUCCESS
193 message = _('Invitation sent to %s' % invitation.username)
195 except SendMailError, e:
197 transaction.rollback()
199 status = messages.ERROR
200 message = _('No invitations left')
201 messages.add_message(request, status, message)
203 sent = [{'email': inv.username,
204 'realname': inv.realname,
205 'is_consumed': inv.is_consumed}
206 for inv in inviter.invitations_sent.all()]
207 kwargs = {'inviter': inviter,
209 context = get_context(request, extra_context, **kwargs)
210 return render_response(template_name,
211 invitation_form = form,
212 context_instance = context)
215 @signed_terms_required
216 def edit_profile(request, template_name='im/profile.html', extra_context={}):
218 Allows a user to edit his/her profile.
220 In case of GET request renders a form for displaying the user information.
221 In case of POST updates the user informantion and redirects to ``next``
222 url parameter if exists.
224 If the user isn't logged in, redirects to settings.LOGIN_URL.
229 A custom template to use. This is optional; if not specified,
230 this will default to ``im/profile.html``.
233 An dictionary of variables to add to the template context.
237 im/profile.html or ``template_name`` keyword argument.
241 The view expectes the following settings are defined:
243 * LOGIN_URL: login uri
245 form = ProfileForm(instance=request.user)
246 extra_context['next'] = request.GET.get('next')
248 if request.method == 'POST':
249 form = ProfileForm(request.POST, instance=request.user)
252 prev_token = request.user.auth_token
254 reset_cookie = user.auth_token != prev_token
255 form = ProfileForm(instance=user)
256 next = request.POST.get('next')
258 return redirect(next)
259 msg = _('Profile has been updated successfully')
260 messages.add_message(request, messages.SUCCESS, msg)
261 except ValueError, ve:
262 messages.add_message(request, messages.ERROR, ve)
263 return render_response(template_name,
264 reset_cookie = reset_cookie,
266 context_instance = get_context(request,
269 @transaction.commit_manually
270 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
272 Allows a user to create a local account.
274 In case of GET request renders a form for providing the user information.
275 In case of POST handles the signup.
277 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
278 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
279 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
280 (see activation_backends);
282 Upon successful user creation if ``next`` url parameter is present the user is redirected there
283 otherwise renders the same page with a success message.
285 On unsuccessful creation, renders ``template_name`` with an error message.
290 A custom template to render. This is optional;
291 if not specified, this will default to ``im/signup.html``.
295 A custom template to render in case of success. This is optional;
296 if not specified, this will default to ``im/signup_complete.html``.
299 An dictionary of variables to add to the template context.
303 im/signup.html or ``template_name`` keyword argument.
304 im/signup_complete.html or ``on_success`` keyword argument.
306 if request.user.is_authenticated():
307 return HttpResponseRedirect(reverse('astakos.im.views.index'))
309 backend = get_backend(request)
311 query_dict = request.__getattribute__(request.method)
312 provider = query_dict.get('provider', 'local')
313 form = backend.get_signup_form(provider)
314 except (Invitation.DoesNotExist, ValueError), e:
315 messages.add_message(request, messages.ERROR, e)
316 if request.method == 'POST':
320 result = backend.handle_activation(user)
321 except SendMailError, e:
323 status = messages.ERROR
324 transaction.rollback()
326 message = result.message
327 status = messages.SUCCESS
329 if user and user.is_active:
330 next = request.POST.get('next', '')
331 return prepare_response(request, user, next=next)
332 messages.add_message(request, status, message)
333 return render_response(on_success,
334 context_instance=get_context(request, extra_context))
335 return render_response(template_name,
336 local_signup_form = form,
337 context_instance=get_context(request, extra_context))
340 @signed_terms_required
341 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
343 Allows a user to send feedback.
345 In case of GET request renders a form for providing the feedback information.
346 In case of POST sends an email to support team.
348 If the user isn't logged in, redirects to settings.LOGIN_URL.
353 A custom template to use. This is optional; if not specified,
354 this will default to ``im/feedback.html``.
357 An dictionary of variables to add to the template context.
361 im/signup.html or ``template_name`` keyword argument.
365 * LOGIN_URL: login uri
366 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
368 if request.method == 'GET':
369 form = FeedbackForm()
370 if request.method == 'POST':
372 return HttpResponse('Unauthorized', status=401)
374 form = FeedbackForm(request.POST)
376 msg = form.cleaned_data['feedback_msg'],
377 data = form.cleaned_data['feedback_data']
379 send_feedback(msg, data, request.user, email_template_name)
380 except SendMailError, e:
382 status = messages.ERROR
384 message = _('Feedback successfully sent')
385 status = messages.SUCCESS
386 messages.add_message(request, status, message)
387 return render_response(template_name,
388 feedback_form = form,
389 context_instance = get_context(request, extra_context))
391 def logout(request, template='registration/logged_out.html', extra_context={}):
393 Wraps `django.contrib.auth.logout` and delete the cookie.
396 response = HttpResponse()
397 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
398 next = request.GET.get('next')
400 response['Location'] = next
401 response.status_code = 302
404 response['Location'] = LOGOUT_NEXT
405 response.status_code = 301
407 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
408 context = get_context(request, extra_context)
409 response.write(render_to_string(template, context_instance=context))
412 @transaction.commit_manually
413 def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
415 Activates the user identified by the ``auth`` request parameter, sends a welcome email
416 and renews the user token.
418 The view uses commit_manually decorator in order to ensure the user state will be updated
419 only if the email will be send successfully.
421 token = request.GET.get('auth')
422 next = request.GET.get('next')
424 user = AstakosUser.objects.get(auth_token=token)
425 except AstakosUser.DoesNotExist:
426 return HttpResponseBadRequest(_('No such user'))
428 user.is_active = True
429 user.email_verified = True
432 send_greeting(user, email_template_name)
433 response = prepare_response(request, user, next, renew=True)
436 except SendEmailError, e:
438 messages.add_message(request, messages.ERROR, message)
439 transaction.rollback()
440 return signup(request, on_failure='im/signup.html')
442 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
447 term = ApprovalTerms.objects.order_by('-id')[0]
452 term = ApprovalTerms.objects.get(id=term_id)
453 except ApprovalTermDoesNotExist, e:
457 return HttpResponseBadRequest(_('No approval terms found.'))
458 f = open(term.location, 'r')
461 if request.method == 'POST':
462 next = request.POST.get('next')
464 next = reverse('astakos.im.views.index')
465 form = SignApprovalTermsForm(request.POST, instance=request.user)
466 if not form.is_valid():
467 return render_response(template_name,
469 approval_terms_form = form,
470 context_instance = get_context(request, extra_context))
472 return HttpResponseRedirect(next)
475 if request.user.is_authenticated() and not has_signed_terms(request.user):
476 form = SignApprovalTermsForm(instance=request.user)
477 return render_response(template_name,
479 approval_terms_form = form,
480 context_instance = get_context(request, extra_context))
482 @signed_terms_required
483 def change_password(request):
484 return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))