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
57 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
58 from astakos.im.activation_backends import get_backend, SimpleBackend
59 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
60 from astakos.im.forms import *
61 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
62 invite as invite_func, logout as auth_logout, send_helpdesk_notification
63 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
65 logger = logging.getLogger(__name__)
67 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
69 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
70 keyword argument and returns an ``django.http.HttpResponse`` with the
74 tab = template.partition('_')[0].partition('.html')[0]
75 kwargs.setdefault('tab', tab)
76 html = render_to_string(template, kwargs, context_instance=context_instance)
77 response = HttpResponse(html, status=status)
79 set_cookie(response, context_instance['request'].user)
83 def requires_anonymous(func):
85 Decorator checkes whether the request.user is not Anonymous and in that case
86 redirects to `logout`.
89 def wrapper(request, *args):
90 if not request.user.is_anonymous():
91 next = urlencode({'next': request.build_absolute_uri()})
92 logout_uri = reverse(logout) + '?' + next
93 return HttpResponseRedirect(logout_uri)
94 return func(request, *args)
97 def signed_terms_required(func):
99 Decorator checkes whether the request.user is Anonymous and in that case
100 redirects to `logout`.
103 def wrapper(request, *args, **kwargs):
104 if request.user.is_authenticated() and not request.user.signed_terms():
105 params = urlencode({'next': request.build_absolute_uri(),
107 terms_uri = reverse('latest_terms') + '?' + params
108 return HttpResponseRedirect(terms_uri)
109 return func(request, *args, **kwargs)
112 @signed_terms_required
113 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
115 If there is logged on user renders the profile page otherwise renders login page.
119 ``login_template_name``
120 A custom login template to use. This is optional; if not specified,
121 this will default to ``im/login.html``.
123 ``profile_template_name``
124 A custom profile template to use. This is optional; if not specified,
125 this will default to ``im/profile.html``.
128 An dictionary of variables to add to the template context.
132 im/profile.html or im/login.html or ``template_name`` keyword argument.
135 template_name = login_template_name
136 if request.user.is_authenticated():
137 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
138 return render_response(template_name,
139 login_form = LoginForm(request=request),
140 context_instance = get_context(request, extra_context))
143 @signed_terms_required
144 @transaction.commit_manually
145 def invite(request, template_name='im/invitations.html', extra_context={}):
147 Allows a user to invite somebody else.
149 In case of GET request renders a form for providing the invitee information.
150 In case of POST checks whether the user has not run out of invitations and then
151 sends an invitation email to singup to the service.
153 The view uses commit_manually decorator in order to ensure the number of the
154 user invitations is going to be updated only if the email has been successfully sent.
156 If the user isn't logged in, redirects to settings.LOGIN_URL.
161 A custom template to use. This is optional; if not specified,
162 this will default to ``im/invitations.html``.
165 An dictionary of variables to add to the template context.
169 im/invitations.html or ``template_name`` keyword argument.
173 The view expectes the following settings are defined:
175 * LOGIN_URL: login uri
176 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
177 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
181 form = InvitationForm()
183 inviter = request.user
184 if request.method == 'POST':
185 form = InvitationForm(request.POST)
186 if inviter.invitations > 0:
189 invitation = form.save()
190 invite_func(invitation, inviter)
191 status = messages.SUCCESS
192 message = _('Invitation sent to %s' % invitation.username)
193 except SendMailError, e:
194 status = messages.ERROR
196 transaction.rollback()
197 except BaseException, e:
198 status = messages.ERROR
199 message = _('Something went wrong.')
201 transaction.rollback()
205 status = messages.ERROR
206 message = _('No invitations left')
207 messages.add_message(request, status, message)
209 sent = [{'email': inv.username,
210 'realname': inv.realname,
211 'is_consumed': inv.is_consumed}
212 for inv in request.user.invitations_sent.all()]
213 kwargs = {'inviter': inviter,
215 context = get_context(request, extra_context, **kwargs)
216 return render_response(template_name,
217 invitation_form = form,
218 context_instance = context)
221 @signed_terms_required
222 def edit_profile(request, template_name='im/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 and redirects to ``next``
228 url parameter if exists.
230 If the user isn't logged in, redirects to settings.LOGIN_URL.
235 A custom template to use. This is optional; if not specified,
236 this will default to ``im/profile.html``.
239 An dictionary of variables to add to the template context.
243 im/profile.html or ``template_name`` keyword argument.
247 The view expectes the following settings are defined:
249 * LOGIN_URL: login uri
251 form = ProfileForm(instance=request.user)
252 extra_context['next'] = request.GET.get('next')
254 if request.method == 'POST':
255 form = ProfileForm(request.POST, instance=request.user)
258 prev_token = request.user.auth_token
260 reset_cookie = user.auth_token != prev_token
261 form = ProfileForm(instance=user)
262 next = request.POST.get('next')
264 return redirect(next)
265 msg = _('Profile has been updated successfully')
266 messages.add_message(request, messages.SUCCESS, msg)
267 except ValueError, ve:
268 messages.add_message(request, messages.ERROR, ve)
269 elif request.method == "GET":
270 request.user.is_verified = True
272 return render_response(template_name,
273 reset_cookie = reset_cookie,
275 context_instance = get_context(request,
278 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
280 Allows a user to create a local account.
282 In case of GET request renders a form for entering the user information.
283 In case of POST handles the signup.
285 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
286 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
287 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
288 (see activation_backends);
290 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
291 otherwise renders the same page with a success message.
293 On unsuccessful creation, renders ``template_name`` with an error message.
298 A custom template to render. This is optional;
299 if not specified, this will default to ``im/signup.html``.
302 A custom template to render in case of success. This is optional;
303 if not specified, this will default to ``im/signup_complete.html``.
306 An dictionary of variables to add to the template context.
310 im/signup.html or ``template_name`` keyword argument.
311 im/signup_complete.html or ``on_success`` keyword argument.
313 if request.user.is_authenticated():
314 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
316 provider = get_query(request).get('provider', 'local')
319 backend = get_backend(request)
320 form = backend.get_signup_form(provider)
322 form = SimpleBackend(request).get_signup_form(provider)
323 messages.add_message(request, messages.ERROR, e)
324 if request.method == 'POST':
326 user = form.save(commit=False)
328 result = backend.handle_activation(user)
329 status = messages.SUCCESS
330 message = result.message
332 if 'additional_email' in form.cleaned_data:
333 additional_email = form.cleaned_data['additional_email']
334 if additional_email != user.email:
335 user.additionalmail_set.create(email=additional_email)
336 msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
337 logger._log(LOGGING_LEVEL, msg, [])
338 if user and user.is_active:
339 next = request.POST.get('next', '')
340 return prepare_response(request, user, next=next)
341 messages.add_message(request, status, message)
342 return render_response(on_success,
343 context_instance=get_context(request, extra_context))
344 except SendMailError, e:
345 status = messages.ERROR
347 messages.add_message(request, status, message)
348 except BaseException, e:
349 status = messages.ERROR
350 message = _('Something went wrong.')
351 messages.add_message(request, status, message)
353 return render_response(template_name,
356 context_instance=get_context(request, extra_context))
359 @signed_terms_required
360 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
362 Allows a user to send feedback.
364 In case of GET request renders a form for providing the feedback information.
365 In case of POST sends an email to support team.
367 If the user isn't logged in, redirects to settings.LOGIN_URL.
372 A custom template to use. This is optional; if not specified,
373 this will default to ``im/feedback.html``.
376 An dictionary of variables to add to the template context.
380 im/signup.html or ``template_name`` keyword argument.
384 * LOGIN_URL: login uri
385 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
387 if request.method == 'GET':
388 form = FeedbackForm()
389 if request.method == 'POST':
391 return HttpResponse('Unauthorized', status=401)
393 form = FeedbackForm(request.POST)
395 msg = form.cleaned_data['feedback_msg']
396 data = form.cleaned_data['feedback_data']
398 send_feedback(msg, data, request.user, email_template_name)
399 except SendMailError, e:
401 status = messages.ERROR
403 message = _('Feedback successfully sent')
404 status = messages.SUCCESS
405 messages.add_message(request, status, message)
406 return render_response(template_name,
407 feedback_form = form,
408 context_instance = get_context(request, extra_context))
410 def logout(request, template='registration/logged_out.html', extra_context={}):
412 Wraps `django.contrib.auth.logout` and delete the cookie.
414 msg = 'Cookie deleted for %s' % (request.user.email)
416 response = HttpResponse()
417 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
418 logger._log(LOGGING_LEVEL, msg, [])
419 next = request.GET.get('next')
421 response['Location'] = next
422 response.status_code = 302
425 response['Location'] = LOGOUT_NEXT
426 response.status_code = 301
428 messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
429 context = get_context(request, extra_context)
430 response.write(render_to_string(template, context_instance=context))
433 @transaction.commit_manually
434 def activate(request, email_template_name='im/welcome_email.txt', on_failure='im/signup.html',
435 helpdesk_email_template_name='im/helpdesk_notification.txt'):
437 Activates the user identified by the ``auth`` request parameter, sends a welcome email
438 and renews the user token.
440 The view uses commit_manually decorator in order to ensure the user state will be updated
441 only if the email will be send successfully.
443 token = request.GET.get('auth')
444 next = request.GET.get('next')
446 user = AstakosUser.objects.get(auth_token=token)
447 except AstakosUser.DoesNotExist:
448 return HttpResponseBadRequest(_('No such user'))
451 message = 'Account already active.'
452 messages.add_message(request, messages.ERROR, message)
453 return render_response(on_failure)
455 notify_helpdesk = False
457 local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
458 except AstakosUser.DoesNotExist:
459 user.is_active = True
460 user.email_verified = True
463 except ValidationError, e:
464 return HttpResponseBadRequest(e)
465 notify_helpdesk = True
467 # switch the existing account to shibboleth one
468 if user.provider == 'shibboleth':
469 local_user.provider = 'shibboleth'
470 local_user.set_unusable_password()
471 local_user.third_party_identifier = user.third_party_identifier
474 except ValidationError, e:
475 return HttpResponseBadRequest(e)
481 send_helpdesk_notification(user, helpdesk_email_template_name)
482 send_greeting(user, email_template_name)
483 response = prepare_response(request, user, next, renew=True)
486 except SendMailError, e:
488 messages.add_message(request, messages.ERROR, message)
489 transaction.rollback()
490 return render_response(on_failure)
491 except BaseException, e:
492 status = messages.ERROR
493 message = _('Something went wrong.')
494 messages.add_message(request, messages.ERROR, message)
496 transaction.rollback()
497 return signup(request, on_failure)
499 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
504 term = ApprovalTerms.objects.order_by('-id')[0]
509 term = ApprovalTerms.objects.get(id=term_id)
510 except ApprovalTermDoesNotExist, e:
514 return HttpResponseRedirect(reverse('astakos.im.views.index'))
515 f = open(term.location, 'r')
518 if request.method == 'POST':
519 next = request.POST.get('next')
521 next = reverse('astakos.im.views.index')
522 form = SignApprovalTermsForm(request.POST, instance=request.user)
523 if not form.is_valid():
524 return render_response(template_name,
526 approval_terms_form = form,
527 context_instance = get_context(request, extra_context))
529 return HttpResponseRedirect(next)
532 if request.user.is_authenticated() and not request.user.signed_terms():
533 form = SignApprovalTermsForm(instance=request.user)
534 return render_response(template_name,
536 approval_terms_form = form,
537 context_instance = get_context(request, extra_context))
539 @signed_terms_required
540 def change_password(request):
541 return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
543 @transaction.commit_manually
544 def change_email(request, activation_key=None,
545 email_template_name='registration/email_change_email.txt',
546 form_template_name='registration/email_change_form.html',
547 confirm_template_name='registration/email_change_done.html',
551 user = EmailChange.objects.change_email(activation_key)
552 if request.user.is_authenticated() and request.user == user:
553 msg = _('Email changed successfully.')
554 messages.add_message(request, messages.SUCCESS, msg)
556 response = prepare_response(request, user)
559 except ValueError, e:
560 messages.add_message(request, messages.ERROR, e)
561 return render_response(confirm_template_name,
562 modified_user = user if 'user' in locals() else None,
563 context_instance = get_context(request,
566 if not request.user.is_authenticated():
567 path = quote(request.get_full_path())
568 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
569 return HttpResponseRedirect(url + '?next=' + path)
570 form = EmailChangeForm(request.POST or None)
571 if request.method == 'POST' and form.is_valid():
573 ec = form.save(email_template_name, request)
574 except SendMailError, e:
575 status = messages.ERROR
577 transaction.rollback()
578 except IntegrityError, e:
579 status = messages.ERROR
580 msg = _('There is already a pending change email request.')
582 status = messages.SUCCESS
583 msg = _('Change email request has been registered succefully.\
584 You are going to receive a verification email in the new address.')
586 messages.add_message(request, status, msg)
587 return render_response(form_template_name,
589 context_instance = get_context(request,