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.views.decorators.http import require_http_methods
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 (
60 get_context, prepare_response, set_cookie, get_query, restrict_next
62 from astakos.im.forms import *
63 from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
64 invite as invite_func, logout as auth_logout, activate as activate_func,
65 send_activation as send_activation_func
67 from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL,
68 COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
71 logger = logging.getLogger(__name__)
73 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
75 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
76 keyword argument and returns an ``django.http.HttpResponse`` with the
80 tab = template.partition('_')[0].partition('.html')[0]
81 kwargs.setdefault('tab', tab)
82 html = render_to_string(template, kwargs, context_instance=context_instance)
83 response = HttpResponse(html, status=status)
85 set_cookie(response, context_instance['request'].user)
89 def requires_anonymous(func):
91 Decorator checkes whether the request.user is not Anonymous and in that case
92 redirects to `logout`.
95 def wrapper(request, *args):
96 if not request.user.is_anonymous():
97 next = urlencode({'next': request.build_absolute_uri()})
98 logout_uri = reverse(logout) + '?' + next
99 return HttpResponseRedirect(logout_uri)
100 return func(request, *args)
103 def signed_terms_required(func):
105 Decorator checkes whether the request.user is Anonymous and in that case
106 redirects to `logout`.
109 def wrapper(request, *args, **kwargs):
110 if request.user.is_authenticated() and not request.user.signed_terms():
111 params = urlencode({'next': request.build_absolute_uri(),
113 terms_uri = reverse('latest_terms') + '?' + params
114 return HttpResponseRedirect(terms_uri)
115 return func(request, *args, **kwargs)
118 @require_http_methods(["GET", "POST"])
119 @signed_terms_required
120 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
122 If there is logged on user renders the profile page otherwise renders login page.
126 ``login_template_name``
127 A custom login template to use. This is optional; if not specified,
128 this will default to ``im/login.html``.
130 ``profile_template_name``
131 A custom profile template to use. This is optional; if not specified,
132 this will default to ``im/profile.html``.
135 An dictionary of variables to add to the template context.
139 im/profile.html or im/login.html or ``template_name`` keyword argument.
142 extra_context = extra_context or {}
143 template_name = login_template_name
144 if request.user.is_authenticated():
145 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
147 return render_response(
149 login_form = LoginForm(request=request),
150 context_instance = get_context(request, extra_context)
153 @require_http_methods(["GET", "POST"])
155 @signed_terms_required
156 @transaction.commit_manually
157 def invite(request, template_name='im/invitations.html', extra_context=None):
159 Allows a user to invite somebody else.
161 In case of GET request renders a form for providing the invitee information.
162 In case of POST checks whether the user has not run out of invitations and then
163 sends an invitation email to singup to the service.
165 The view uses commit_manually decorator in order to ensure the number of the
166 user invitations is going to be updated only if the email has been successfully sent.
168 If the user isn't logged in, redirects to settings.LOGIN_URL.
173 A custom template to use. This is optional; if not specified,
174 this will default to ``im/invitations.html``.
177 An dictionary of variables to add to the template context.
181 im/invitations.html or ``template_name`` keyword argument.
185 The view expectes the following settings are defined:
187 * LOGIN_URL: login uri
188 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
189 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
191 extra_context = extra_context or {}
194 form = InvitationForm()
196 inviter = request.user
197 if request.method == 'POST':
198 form = InvitationForm(request.POST)
199 if inviter.invitations > 0:
202 invitation = form.save()
203 invite_func(invitation, inviter)
204 status = messages.SUCCESS
205 message = _('Invitation sent to %s' % invitation.username)
206 except SendMailError, e:
207 status = messages.ERROR
209 transaction.rollback()
210 except BaseException, e:
211 status = messages.ERROR
212 message = _('Something went wrong.')
214 transaction.rollback()
218 status = messages.ERROR
219 message = _('No invitations left')
220 messages.add_message(request, status, message)
222 sent = [{'email': inv.username,
223 'realname': inv.realname,
224 'is_consumed': inv.is_consumed}
225 for inv in request.user.invitations_sent.all()]
226 kwargs = {'inviter': inviter,
228 context = get_context(request, extra_context, **kwargs)
229 return render_response(template_name,
230 invitation_form = form,
231 context_instance = context)
233 @require_http_methods(["GET", "POST"])
235 @signed_terms_required
236 def edit_profile(request, template_name='im/profile.html', extra_context=None):
238 Allows a user to edit his/her profile.
240 In case of GET request renders a form for displaying the user information.
241 In case of POST updates the user informantion and redirects to ``next``
242 url parameter if exists.
244 If the user isn't logged in, redirects to settings.LOGIN_URL.
249 A custom template to use. This is optional; if not specified,
250 this will default to ``im/profile.html``.
253 An dictionary of variables to add to the template context.
257 im/profile.html or ``template_name`` keyword argument.
261 The view expectes the following settings are defined:
263 * LOGIN_URL: login uri
265 extra_context = extra_context or {}
266 form = ProfileForm(instance=request.user)
267 extra_context['next'] = request.GET.get('next')
269 if request.method == 'POST':
270 form = ProfileForm(request.POST, instance=request.user)
273 prev_token = request.user.auth_token
275 reset_cookie = user.auth_token != prev_token
276 form = ProfileForm(instance=user)
277 next = restrict_next(
278 request.POST.get('next'),
282 return redirect(next)
283 msg = _('<p>Profile has been updated successfully</p>')
284 messages.add_message(request, messages.SUCCESS, msg)
285 except ValueError, ve:
286 messages.add_message(request, messages.ERROR, ve)
287 elif request.method == "GET":
288 request.user.is_verified = True
290 return render_response(template_name,
291 reset_cookie = reset_cookie,
293 context_instance = get_context(request,
296 @require_http_methods(["GET", "POST"])
297 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
299 Allows a user to create a local account.
301 In case of GET request renders a form for entering the user information.
302 In case of POST handles the signup.
304 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
305 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
306 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
307 (see activation_backends);
309 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
310 otherwise renders the same page with a success message.
312 On unsuccessful creation, renders ``template_name`` with an error message.
317 A custom template to render. This is optional;
318 if not specified, this will default to ``im/signup.html``.
321 A custom template to render in case of success. This is optional;
322 if not specified, this will default to ``im/signup_complete.html``.
325 An dictionary of variables to add to the template context.
329 im/signup.html or ``template_name`` keyword argument.
330 im/signup_complete.html or ``on_success`` keyword argument.
332 extra_context = extra_context or {}
333 if request.user.is_authenticated():
334 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
336 provider = get_query(request).get('provider', 'local')
339 backend = get_backend(request)
340 form = backend.get_signup_form(provider)
342 form = SimpleBackend(request).get_signup_form(provider)
343 messages.add_message(request, messages.ERROR, e)
344 if request.method == 'POST':
346 user = form.save(commit=False)
348 result = backend.handle_activation(user)
349 status = messages.SUCCESS
350 message = result.message
352 if 'additional_email' in form.cleaned_data:
353 additional_email = form.cleaned_data['additional_email']
354 if additional_email != user.email:
355 user.additionalmail_set.create(email=additional_email)
356 msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
357 logger._log(LOGGING_LEVEL, msg, [])
358 if user and user.is_active:
359 next = request.POST.get('next', '')
360 return prepare_response(request, user, next=next)
361 messages.add_message(request, status, message)
362 return render_response(on_success,
363 context_instance=get_context(request, extra_context))
364 except SendMailError, e:
366 status = messages.ERROR
368 messages.add_message(request, status, message)
369 except BaseException, e:
371 status = messages.ERROR
372 message = _('Something went wrong.')
373 messages.add_message(request, status, message)
375 return render_response(template_name,
378 context_instance=get_context(request, extra_context))
380 @require_http_methods(["GET", "POST"])
382 @signed_terms_required
383 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
385 Allows a user to send feedback.
387 In case of GET request renders a form for providing the feedback information.
388 In case of POST sends an email to support team.
390 If the user isn't logged in, redirects to settings.LOGIN_URL.
395 A custom template to use. This is optional; if not specified,
396 this will default to ``im/feedback.html``.
399 An dictionary of variables to add to the template context.
403 im/signup.html or ``template_name`` keyword argument.
407 * LOGIN_URL: login uri
408 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
410 extra_context = extra_context or {}
411 if request.method == 'GET':
412 form = FeedbackForm()
413 if request.method == 'POST':
415 return HttpResponse('Unauthorized', status=401)
417 form = FeedbackForm(request.POST)
419 msg = form.cleaned_data['feedback_msg']
420 data = form.cleaned_data['feedback_data']
422 send_feedback(msg, data, request.user, email_template_name)
423 except SendMailError, e:
425 status = messages.ERROR
427 message = _('Feedback successfully sent')
428 status = messages.SUCCESS
429 messages.add_message(request, status, message)
430 return render_response(template_name,
431 feedback_form = form,
432 context_instance = get_context(request, extra_context))
434 @require_http_methods(["GET"])
435 def logout(request, template='registration/logged_out.html', extra_context=None):
437 Wraps `django.contrib.auth.logout` and delete the cookie.
439 extra_context = extra_context or {}
440 response = HttpResponse()
442 if request.user.is_authenticated():
443 email = request.user.email
445 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
446 msg = 'Cookie deleted for %s' % email
447 next = restrict_next(
448 request.GET.get('next'),
452 response['Location'] = next
453 response.status_code = 302
455 logger._log(LOGGING_LEVEL, msg, [])
458 response['Location'] = LOGOUT_NEXT
459 response.status_code = 301
461 logger._log(LOGGING_LEVEL, msg, [])
463 messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
464 context = get_context(request, extra_context)
465 response.write(render_to_string(template, context_instance=context))
467 logger._log(LOGGING_LEVEL, msg, [])
470 @require_http_methods(["GET", "POST"])
471 @transaction.commit_manually
472 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
474 Activates the user identified by the ``auth`` request parameter, sends a welcome email
475 and renews the user token.
477 The view uses commit_manually decorator in order to ensure the user state will be updated
478 only if the email will be send successfully.
480 token = request.GET.get('auth')
481 next = request.GET.get('next')
483 user = AstakosUser.objects.get(auth_token=token)
484 except AstakosUser.DoesNotExist:
485 return HttpResponseBadRequest(_('No such user'))
488 message = _('Account already active.')
489 messages.add_message(request, messages.ERROR, message)
490 return index(request)
493 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
494 response = prepare_response(request, user, next, renew=True)
497 except SendMailError, e:
499 messages.add_message(request, messages.ERROR, message)
500 transaction.rollback()
501 return index(request)
502 except BaseException, e:
503 status = messages.ERROR
504 message = _('Something went wrong.')
505 messages.add_message(request, messages.ERROR, message)
507 transaction.rollback()
508 return index(request)
510 @require_http_methods(["GET", "POST"])
511 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
512 extra_context = extra_context or {}
517 term = ApprovalTerms.objects.order_by('-id')[0]
522 term = ApprovalTerms.objects.get(id=term_id)
523 except ApprovalTermDoesNotExist, e:
527 return HttpResponseRedirect(reverse('astakos.im.views.index'))
528 f = open(term.location, 'r')
531 if request.method == 'POST':
532 next = restrict_next(
533 request.POST.get('next'),
537 next = reverse('astakos.im.views.index')
538 form = SignApprovalTermsForm(request.POST, instance=request.user)
539 if not form.is_valid():
540 return render_response(template_name,
542 approval_terms_form = form,
543 context_instance = get_context(request, extra_context))
545 return HttpResponseRedirect(next)
548 if request.user.is_authenticated() and not request.user.signed_terms():
549 form = SignApprovalTermsForm(instance=request.user)
550 return render_response(template_name,
552 approval_terms_form = form,
553 context_instance = get_context(request, extra_context))
555 @require_http_methods(["GET", "POST"])
556 @signed_terms_required
557 def change_password(request):
558 return password_change(request,
559 post_change_redirect=reverse('astakos.im.views.edit_profile'),
560 password_change_form=ExtendedPasswordChangeForm)
562 @require_http_methods(["GET", "POST"])
564 @signed_terms_required
565 @transaction.commit_manually
566 def change_email(request, activation_key=None,
567 email_template_name='registration/email_change_email.txt',
568 form_template_name='registration/email_change_form.html',
569 confirm_template_name='registration/email_change_done.html',
571 extra_context = extra_context or {}
574 user = EmailChange.objects.change_email(activation_key)
575 if request.user.is_authenticated() and request.user == user:
576 msg = _('Email changed successfully.')
577 messages.add_message(request, messages.SUCCESS, msg)
579 response = prepare_response(request, user)
582 except ValueError, e:
583 messages.add_message(request, messages.ERROR, e)
584 return render_response(confirm_template_name,
585 modified_user = user if 'user' in locals() else None,
586 context_instance = get_context(request,
589 if not request.user.is_authenticated():
590 path = quote(request.get_full_path())
591 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
592 return HttpResponseRedirect(url + '?next=' + path)
593 form = EmailChangeForm(request.POST or None)
594 if request.method == 'POST' and form.is_valid():
596 ec = form.save(email_template_name, request)
597 except SendMailError, e:
598 status = messages.ERROR
600 transaction.rollback()
601 except IntegrityError, e:
602 status = messages.ERROR
603 msg = _('There is already a pending change email request.')
605 status = messages.SUCCESS
606 msg = _('Change email request has been registered succefully.\
607 You are going to receive a verification email in the new address.')
609 messages.add_message(request, status, msg)
610 return render_response(form_template_name,
614 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
615 extra_context = extra_context or {}
617 u = AstakosUser.objects.get(id=user_id)
618 except AstakosUser.DoesNotExist:
619 messages.error(request, _('Invalid user id'))
622 send_activation_func(u)
623 msg = _('Activation sent.')
624 messages.success(request, msg)
625 except SendMailError, e:
626 messages.error(request, e)
627 return render_response(
629 login_form = LoginForm(request=request),
630 context_instance = get_context(