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, HttpResponseBadRequest
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.utils.http import urlencode
51 from django.http import HttpResponseRedirect, HttpResponseBadRequest
52 from django.db.utils import IntegrityError
53 from django.contrib.auth.views import password_change
54 from django.core.exceptions import ValidationError
56 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
57 from astakos.im.activation_backends import get_backend, SimpleBackend
58 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
59 from astakos.im.forms import *
60 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
61 invite as invite_func, logout as auth_logout
62 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT
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 request.user.signed_terms():
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(request=request),
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 form = InvitationForm()
182 inviter = request.user
183 if request.method == 'POST':
184 form = InvitationForm(request.POST)
185 if inviter.invitations > 0:
188 invitation = form.save()
189 invite_func(invitation, inviter)
190 status = messages.SUCCESS
191 message = _('Invitation sent to %s' % invitation.username)
192 except SendMailError, e:
193 status = messages.ERROR
195 transaction.rollback()
196 except BaseException, e:
197 status = messages.ERROR
198 message = _('Something went wrong.')
200 transaction.rollback()
204 status = messages.ERROR
205 message = _('No invitations left')
206 messages.add_message(request, status, message)
208 sent = [{'email': inv.username,
209 'realname': inv.realname,
210 'is_consumed': inv.is_consumed}
211 for inv in request.user.invitations_sent.all()]
212 kwargs = {'inviter': inviter,
214 context = get_context(request, extra_context, **kwargs)
215 return render_response(template_name,
216 invitation_form = form,
217 context_instance = context)
220 @signed_terms_required
221 def edit_profile(request, template_name='im/profile.html', extra_context={}):
223 Allows a user to edit his/her profile.
225 In case of GET request renders a form for displaying the user information.
226 In case of POST updates the user informantion and redirects to ``next``
227 url parameter if exists.
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 ``im/profile.html``.
238 An dictionary of variables to add to the template context.
242 im/profile.html or ``template_name`` keyword argument.
246 The view expectes the following settings are defined:
248 * LOGIN_URL: login uri
250 form = ProfileForm(instance=request.user)
251 extra_context['next'] = request.GET.get('next')
253 if request.method == 'POST':
254 form = ProfileForm(request.POST, instance=request.user)
257 prev_token = request.user.auth_token
259 reset_cookie = user.auth_token != prev_token
260 form = ProfileForm(instance=user)
261 next = request.POST.get('next')
263 return redirect(next)
264 msg = _('Profile has been updated successfully')
265 messages.add_message(request, messages.SUCCESS, msg)
266 except ValueError, ve:
267 messages.add_message(request, messages.ERROR, ve)
268 return render_response(template_name,
269 reset_cookie = reset_cookie,
271 context_instance = get_context(request,
274 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
276 Allows a user to create a local account.
278 In case of GET request renders a form for providing the user information.
279 In case of POST handles the signup.
281 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
282 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
283 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
284 (see activation_backends);
286 Upon successful user creation if ``next`` url parameter is present the user is redirected there
287 otherwise renders the same page with a success message.
289 On unsuccessful creation, renders ``template_name`` with an error message.
294 A custom template to render. This is optional;
295 if not specified, this will default to ``im/signup.html``.
299 A custom template to render in case of success. This is optional;
300 if not specified, this will default to ``im/signup_complete.html``.
303 An dictionary of variables to add to the template context.
307 im/signup.html or ``template_name`` keyword argument.
308 im/signup_complete.html or ``on_success`` keyword argument.
310 if request.user.is_authenticated():
311 return HttpResponseRedirect(reverse('astakos.im.views.index'))
313 provider = get_query(request).get('provider', 'local')
316 backend = get_backend(request)
317 form = backend.get_signup_form(provider)
319 form = SimpleBackend(request).get_signup_form(provider)
320 messages.add_message(request, messages.ERROR, e)
321 if request.method == 'POST':
323 user = form.save(commit=False)
325 result = backend.handle_activation(user)
326 status = messages.SUCCESS
327 message = result.message
329 if 'additional_email' in form.cleaned_data:
330 additional_email = form.cleaned_data['additional_email']
331 if additional_email != user.email:
332 user.additionalmail_set.create(email=additional_email)
333 if user and user.is_active:
334 next = request.POST.get('next', '')
335 return prepare_response(request, user, next=next)
336 messages.add_message(request, status, message)
337 return render_response(on_success,
338 context_instance=get_context(request, extra_context))
339 except SendMailError, e:
340 status = messages.ERROR
342 messages.add_message(request, status, message)
343 except BaseException, e:
344 status = messages.ERROR
345 message = _('Something went wrong.')
346 messages.add_message(request, status, message)
348 return render_response(template_name,
351 context_instance=get_context(request, extra_context))
354 @signed_terms_required
355 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
357 Allows a user to send feedback.
359 In case of GET request renders a form for providing the feedback information.
360 In case of POST sends an email to support team.
362 If the user isn't logged in, redirects to settings.LOGIN_URL.
367 A custom template to use. This is optional; if not specified,
368 this will default to ``im/feedback.html``.
371 An dictionary of variables to add to the template context.
375 im/signup.html or ``template_name`` keyword argument.
379 * LOGIN_URL: login uri
380 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
382 if request.method == 'GET':
383 form = FeedbackForm()
384 if request.method == 'POST':
386 return HttpResponse('Unauthorized', status=401)
388 form = FeedbackForm(request.POST)
390 msg = form.cleaned_data['feedback_msg']
391 data = form.cleaned_data['feedback_data']
393 send_feedback(msg, data, request.user, email_template_name)
394 except SendMailError, e:
396 status = messages.ERROR
398 message = _('Feedback successfully sent')
399 status = messages.SUCCESS
400 messages.add_message(request, status, message)
401 return render_response(template_name,
402 feedback_form = form,
403 context_instance = get_context(request, extra_context))
405 def logout(request, template='registration/logged_out.html', extra_context={}):
407 Wraps `django.contrib.auth.logout` and delete the cookie.
410 response = HttpResponse()
411 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
412 next = request.GET.get('next')
414 response['Location'] = next
415 response.status_code = 302
418 response['Location'] = LOGOUT_NEXT
419 response.status_code = 301
421 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
422 context = get_context(request, extra_context)
423 response.write(render_to_string(template, context_instance=context))
426 @transaction.commit_manually
427 def activate(request, email_template_name='im/welcome_email.txt', on_failure='im/signup.html'):
429 Activates the user identified by the ``auth`` request parameter, sends a welcome email
430 and renews the user token.
432 The view uses commit_manually decorator in order to ensure the user state will be updated
433 only if the email will be send successfully.
435 token = request.GET.get('auth')
436 next = request.GET.get('next')
438 user = AstakosUser.objects.get(auth_token=token)
439 except AstakosUser.DoesNotExist:
440 return HttpResponseBadRequest(_('No such user'))
443 local_user = AstakosUser.objects.get(email=user.email, is_active=True)
444 except AstakosUser.DoesNotExist:
445 user.is_active = True
446 user.email_verified = True
449 except ValidationError, e:
450 return HttpResponseBadRequest(e)
452 # switch the existing account to shibboleth one
453 local_user.provider = 'shibboleth'
454 local_user.set_unusable_password()
455 local_user.third_party_identifier = user.third_party_identifier
458 except ValidationError, e:
459 return HttpResponseBadRequest(e)
464 send_greeting(user, email_template_name)
465 response = prepare_response(request, user, next, renew=True)
468 except SendMailError, e:
470 messages.add_message(request, messages.ERROR, message)
471 transaction.rollback()
472 return render_response(on_failure)
473 except BaseException, e:
474 status = messages.ERROR
475 message = _('Something went wrong.')
476 messages.add_message(request, messages.ERROR, message)
478 transaction.rollback()
479 return signup(request, on_failure)
481 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
486 term = ApprovalTerms.objects.order_by('-id')[0]
491 term = ApprovalTerms.objects.get(id=term_id)
492 except ApprovalTermDoesNotExist, e:
496 return HttpResponseRedirect(reverse('astakos.im.views.index'))
497 f = open(term.location, 'r')
500 if request.method == 'POST':
501 next = request.POST.get('next')
503 next = reverse('astakos.im.views.index')
504 form = SignApprovalTermsForm(request.POST, instance=request.user)
505 if not form.is_valid():
506 return render_response(template_name,
508 approval_terms_form = form,
509 context_instance = get_context(request, extra_context))
511 return HttpResponseRedirect(next)
514 if request.user.is_authenticated() and not request.user.signed_terms():
515 form = SignApprovalTermsForm(instance=request.user)
516 return render_response(template_name,
518 approval_terms_form = form,
519 context_instance = get_context(request, extra_context))
521 @signed_terms_required
522 def change_password(request):
523 return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
525 @transaction.commit_manually
526 def change_email(request, activation_key=None,
527 email_template_name='registration/email_change_email.txt',
528 form_template_name='registration/email_change_form.html',
529 confirm_template_name='registration/email_change_done.html',
533 user = EmailChange.objects.change_email(activation_key)
534 if request.user.is_authenticated() and request.user == user:
535 msg = _('Email changed successfully.')
536 messages.add_message(request, messages.SUCCESS, msg)
538 response = prepare_response(request, user)
541 except ValueError, e:
542 messages.add_message(request, messages.ERROR, e)
543 return render_response(confirm_template_name,
544 modified_user = user if 'user' in locals() else None,
545 context_instance = get_context(request,
548 if not request.user.is_authenticated():
549 path = quote(request.get_full_path())
550 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
551 return HttpResponseRedirect(url + '?next=' + path)
552 form = EmailChangeForm(request.POST or None)
553 if request.method == 'POST' and form.is_valid():
555 ec = form.save(email_template_name, request)
556 except SendMailError, e:
557 status = messages.ERROR
559 transaction.rollback()
560 except IntegrityError, e:
561 status = messages.ERROR
562 msg = _('There is already a pending change email request.')
564 status = messages.SUCCESS
565 msg = _('Change email request has been registered succefully.\
566 You are going to receive a verification email in the new address.')
568 messages.add_message(request, status, msg)
569 return render_response(form_template_name,
571 context_instance = get_context(request,