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
55 from django.db.models import Q
56 from django.views.decorators.http import require_http_methods
58 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
59 from astakos.im.activation_backends import get_backend, SimpleBackend
60 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
61 from astakos.im.forms import *
62 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
63 invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
64 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
66 logger = logging.getLogger(__name__)
68 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
70 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
71 keyword argument and returns an ``django.http.HttpResponse`` with the
75 tab = template.partition('_')[0].partition('.html')[0]
76 kwargs.setdefault('tab', tab)
77 html = render_to_string(template, kwargs, context_instance=context_instance)
78 response = HttpResponse(html, status=status)
80 set_cookie(response, context_instance['request'].user)
84 def requires_anonymous(func):
86 Decorator checkes whether the request.user is not Anonymous and in that case
87 redirects to `logout`.
90 def wrapper(request, *args):
91 if not request.user.is_anonymous():
92 next = urlencode({'next': request.build_absolute_uri()})
93 logout_uri = reverse(logout) + '?' + next
94 return HttpResponseRedirect(logout_uri)
95 return func(request, *args)
98 def signed_terms_required(func):
100 Decorator checkes whether the request.user is Anonymous and in that case
101 redirects to `logout`.
104 def wrapper(request, *args, **kwargs):
105 if request.user.is_authenticated() and not request.user.signed_terms():
106 params = urlencode({'next': request.build_absolute_uri(),
108 terms_uri = reverse('latest_terms') + '?' + params
109 return HttpResponseRedirect(terms_uri)
110 return func(request, *args, **kwargs)
113 @require_http_methods(["GET", "POST"])
114 @signed_terms_required
115 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
117 If there is logged on user renders the profile page otherwise renders login page.
121 ``login_template_name``
122 A custom login template to use. This is optional; if not specified,
123 this will default to ``im/login.html``.
125 ``profile_template_name``
126 A custom profile template to use. This is optional; if not specified,
127 this will default to ``im/profile.html``.
130 An dictionary of variables to add to the template context.
134 im/profile.html or im/login.html or ``template_name`` keyword argument.
137 template_name = login_template_name
138 if request.user.is_authenticated():
139 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
140 return render_response(template_name,
141 login_form = LoginForm(request=request),
142 context_instance = get_context(request, extra_context))
144 @require_http_methods(["GET", "POST"])
146 @signed_terms_required
147 @transaction.commit_manually
148 def invite(request, template_name='im/invitations.html', extra_context={}):
150 Allows a user to invite somebody else.
152 In case of GET request renders a form for providing the invitee information.
153 In case of POST checks whether the user has not run out of invitations and then
154 sends an invitation email to singup to the service.
156 The view uses commit_manually decorator in order to ensure the number of the
157 user invitations is going to be updated only if the email has been successfully sent.
159 If the user isn't logged in, redirects to settings.LOGIN_URL.
164 A custom template to use. This is optional; if not specified,
165 this will default to ``im/invitations.html``.
168 An dictionary of variables to add to the template context.
172 im/invitations.html or ``template_name`` keyword argument.
176 The view expectes the following settings are defined:
178 * LOGIN_URL: login uri
179 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
180 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
184 form = InvitationForm()
186 inviter = request.user
187 if request.method == 'POST':
188 form = InvitationForm(request.POST)
189 if inviter.invitations > 0:
192 invitation = form.save()
193 invite_func(invitation, inviter)
194 status = messages.SUCCESS
195 message = _('Invitation sent to %s' % invitation.username)
196 except SendMailError, e:
197 status = messages.ERROR
199 transaction.rollback()
200 except BaseException, e:
201 status = messages.ERROR
202 message = _('Something went wrong.')
204 transaction.rollback()
208 status = messages.ERROR
209 message = _('No invitations left')
210 messages.add_message(request, status, message)
212 sent = [{'email': inv.username,
213 'realname': inv.realname,
214 'is_consumed': inv.is_consumed}
215 for inv in request.user.invitations_sent.all()]
216 kwargs = {'inviter': inviter,
218 context = get_context(request, extra_context, **kwargs)
219 return render_response(template_name,
220 invitation_form = form,
221 context_instance = context)
223 @require_http_methods(["GET", "POST"])
225 @signed_terms_required
226 def edit_profile(request, template_name='im/profile.html', extra_context={}):
228 Allows a user to edit his/her profile.
230 In case of GET request renders a form for displaying the user information.
231 In case of POST updates the user informantion and redirects to ``next``
232 url parameter if exists.
234 If the user isn't logged in, redirects to settings.LOGIN_URL.
239 A custom template to use. This is optional; if not specified,
240 this will default to ``im/profile.html``.
243 An dictionary of variables to add to the template context.
247 im/profile.html or ``template_name`` keyword argument.
251 The view expectes the following settings are defined:
253 * LOGIN_URL: login uri
255 form = ProfileForm(instance=request.user)
256 extra_context['next'] = request.GET.get('next')
258 if request.method == 'POST':
259 form = ProfileForm(request.POST, instance=request.user)
262 prev_token = request.user.auth_token
264 reset_cookie = user.auth_token != prev_token
265 form = ProfileForm(instance=user)
266 next = request.POST.get('next')
268 return redirect(next)
269 msg = _('<p>Profile has been updated successfully</p>')
270 messages.add_message(request, messages.SUCCESS, msg)
271 except ValueError, ve:
272 messages.add_message(request, messages.ERROR, ve)
273 elif request.method == "GET":
274 request.user.is_verified = True
276 return render_response(template_name,
277 reset_cookie = reset_cookie,
279 context_instance = get_context(request,
282 @require_http_methods(["GET", "POST"])
283 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
285 Allows a user to create a local account.
287 In case of GET request renders a form for entering the user information.
288 In case of POST handles the signup.
290 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
291 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
292 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
293 (see activation_backends);
295 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
296 otherwise renders the same page with a success message.
298 On unsuccessful creation, renders ``template_name`` with an error message.
303 A custom template to render. This is optional;
304 if not specified, this will default to ``im/signup.html``.
307 A custom template to render in case of success. This is optional;
308 if not specified, this will default to ``im/signup_complete.html``.
311 An dictionary of variables to add to the template context.
315 im/signup.html or ``template_name`` keyword argument.
316 im/signup_complete.html or ``on_success`` keyword argument.
318 if request.user.is_authenticated():
319 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
321 provider = get_query(request).get('provider', 'local')
324 backend = get_backend(request)
325 form = backend.get_signup_form(provider)
327 form = SimpleBackend(request).get_signup_form(provider)
328 messages.add_message(request, messages.ERROR, e)
329 if request.method == 'POST':
331 user = form.save(commit=False)
333 result = backend.handle_activation(user)
334 status = messages.SUCCESS
335 message = result.message
337 if 'additional_email' in form.cleaned_data:
338 additional_email = form.cleaned_data['additional_email']
339 if additional_email != user.email:
340 user.additionalmail_set.create(email=additional_email)
341 msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
342 logger._log(LOGGING_LEVEL, msg, [])
343 if user and user.is_active:
344 next = request.POST.get('next', '')
345 return prepare_response(request, user, next=next)
346 messages.add_message(request, status, message)
347 return render_response(on_success,
348 context_instance=get_context(request, extra_context))
349 except SendMailError, e:
350 status = messages.ERROR
352 messages.add_message(request, status, message)
353 except BaseException, e:
354 status = messages.ERROR
355 message = _('Something went wrong.')
356 messages.add_message(request, status, message)
358 return render_response(template_name,
361 context_instance=get_context(request, extra_context))
363 @require_http_methods(["GET", "POST"])
365 @signed_terms_required
366 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
368 Allows a user to send feedback.
370 In case of GET request renders a form for providing the feedback information.
371 In case of POST sends an email to support team.
373 If the user isn't logged in, redirects to settings.LOGIN_URL.
378 A custom template to use. This is optional; if not specified,
379 this will default to ``im/feedback.html``.
382 An dictionary of variables to add to the template context.
386 im/signup.html or ``template_name`` keyword argument.
390 * LOGIN_URL: login uri
391 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
393 if request.method == 'GET':
394 form = FeedbackForm()
395 if request.method == 'POST':
397 return HttpResponse('Unauthorized', status=401)
399 form = FeedbackForm(request.POST)
401 msg = form.cleaned_data['feedback_msg']
402 data = form.cleaned_data['feedback_data']
404 send_feedback(msg, data, request.user, email_template_name)
405 except SendMailError, e:
407 status = messages.ERROR
409 message = _('Feedback successfully sent')
410 status = messages.SUCCESS
411 messages.add_message(request, status, message)
412 return render_response(template_name,
413 feedback_form = form,
414 context_instance = get_context(request, extra_context))
416 @require_http_methods(["GET", "POST"])
417 def logout(request, template='registration/logged_out.html', extra_context={}):
419 Wraps `django.contrib.auth.logout` and delete the cookie.
421 response = HttpResponse()
422 if request.user.is_authenticated():
423 email = request.user.email
425 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
426 msg = 'Cookie deleted for %s' % email
427 logger._log(LOGGING_LEVEL, msg, [])
428 next = request.GET.get('next')
430 response['Location'] = next
431 response.status_code = 302
434 response['Location'] = LOGOUT_NEXT
435 response.status_code = 301
437 messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
438 context = get_context(request, extra_context)
439 response.write(render_to_string(template, context_instance=context))
442 @require_http_methods(["GET", "POST"])
443 @transaction.commit_manually
444 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
446 Activates the user identified by the ``auth`` request parameter, sends a welcome email
447 and renews the user token.
449 The view uses commit_manually decorator in order to ensure the user state will be updated
450 only if the email will be send successfully.
452 token = request.GET.get('auth')
453 next = request.GET.get('next')
455 user = AstakosUser.objects.get(auth_token=token)
456 except AstakosUser.DoesNotExist:
457 return HttpResponseBadRequest(_('No such user'))
460 message = _('Account already active.')
461 messages.add_message(request, messages.ERROR, message)
462 return index(request)
465 local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
466 except AstakosUser.DoesNotExist:
468 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
469 response = prepare_response(request, user, next, renew=True)
472 except SendMailError, e:
474 messages.add_message(request, messages.ERROR, message)
475 transaction.rollback()
476 return index(request)
477 except BaseException, e:
478 status = messages.ERROR
479 message = _('Something went wrong.')
480 messages.add_message(request, messages.ERROR, message)
482 transaction.rollback()
483 return index(request)
486 user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
487 response = prepare_response(request, user, next, renew=True)
490 except SendMailError, e:
492 messages.add_message(request, messages.ERROR, message)
493 transaction.rollback()
494 return index(request)
495 except BaseException, e:
496 status = messages.ERROR
497 message = _('Something went wrong.')
498 messages.add_message(request, messages.ERROR, message)
500 transaction.rollback()
501 return index(request)
503 @require_http_methods(["GET", "POST"])
504 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
509 term = ApprovalTerms.objects.order_by('-id')[0]
514 term = ApprovalTerms.objects.get(id=term_id)
515 except ApprovalTermDoesNotExist, e:
519 return HttpResponseRedirect(reverse('astakos.im.views.index'))
520 f = open(term.location, 'r')
523 if request.method == 'POST':
524 next = request.POST.get('next')
526 next = reverse('astakos.im.views.index')
527 form = SignApprovalTermsForm(request.POST, instance=request.user)
528 if not form.is_valid():
529 return render_response(template_name,
531 approval_terms_form = form,
532 context_instance = get_context(request, extra_context))
534 return HttpResponseRedirect(next)
537 if request.user.is_authenticated() and not request.user.signed_terms():
538 form = SignApprovalTermsForm(instance=request.user)
539 return render_response(template_name,
541 approval_terms_form = form,
542 context_instance = get_context(request, extra_context))
544 @require_http_methods(["GET", "POST"])
545 @signed_terms_required
546 def change_password(request):
547 return password_change(request,
548 post_change_redirect=reverse('astakos.im.views.edit_profile'),
549 password_change_form=ExtendedPasswordChangeForm)
551 @require_http_methods(["GET", "POST"])
553 @signed_terms_required
554 @transaction.commit_manually
555 def change_email(request, activation_key=None,
556 email_template_name='registration/email_change_email.txt',
557 form_template_name='registration/email_change_form.html',
558 confirm_template_name='registration/email_change_done.html',
562 user = EmailChange.objects.change_email(activation_key)
563 if request.user.is_authenticated() and request.user == user:
564 msg = _('Email changed successfully.')
565 messages.add_message(request, messages.SUCCESS, msg)
567 response = prepare_response(request, user)
570 except ValueError, e:
571 messages.add_message(request, messages.ERROR, e)
572 return render_response(confirm_template_name,
573 modified_user = user if 'user' in locals() else None,
574 context_instance = get_context(request,
577 if not request.user.is_authenticated():
578 path = quote(request.get_full_path())
579 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
580 return HttpResponseRedirect(url + '?next=' + path)
581 form = EmailChangeForm(request.POST or None)
582 if request.method == 'POST' and form.is_valid():
584 ec = form.save(email_template_name, request)
585 except SendMailError, e:
586 status = messages.ERROR
588 transaction.rollback()
589 except IntegrityError, e:
590 status = messages.ERROR
591 msg = _('There is already a pending change email request.')
593 status = messages.SUCCESS
594 msg = _('Change email request has been registered succefully.\
595 You are going to receive a verification email in the new address.')
597 messages.add_message(request, status, msg)
598 return render_response(form_template_name,
600 context_instance = get_context(request,