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 (
43 HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
45 from django.shortcuts import redirect
46 from django.template.loader import render_to_string
47 from django.utils.translation import ugettext as _
48 from django.core.urlresolvers import reverse
49 from django.contrib.auth.decorators import login_required
50 from django.contrib import messages
51 from django.db import transaction
52 from django.utils.http import urlencode
53 from django.db.utils import IntegrityError
54 from django.contrib.auth.views import password_change
55 from django.core.exceptions import ValidationError
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 (
61 get_context, prepare_response, get_query, restrict_next
63 from astakos.im.forms import *
64 from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
65 invite as invite_func, logout as auth_logout, activate as activate_func,
66 send_activation as send_activation_func
68 from astakos.im.settings import (
69 DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_DOMAIN, IM_MODULES,
70 SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
73 logger = logging.getLogger(__name__)
75 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
77 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
78 keyword argument and returns an ``django.http.HttpResponse`` with the
82 tab = template.partition('_')[0].partition('.html')[0]
83 kwargs.setdefault('tab', tab)
84 html = render_to_string(template, kwargs, context_instance=context_instance)
85 response = HttpResponse(html, status=status)
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 {}
267 instance=request.user,
268 session_key=request.session.session_key
270 extra_context['next'] = request.GET.get('next')
271 if request.method == 'POST':
274 instance=request.user,
275 session_key=request.session.session_key
279 prev_token = request.user.auth_token
283 session_key=request.session.session_key
285 next = restrict_next(
286 request.POST.get('next'),
290 return redirect(next)
291 msg = _('<p>Profile has been updated successfully</p>')
292 messages.add_message(request, messages.SUCCESS, msg)
293 except ValueError, ve:
294 messages.add_message(request, messages.ERROR, ve)
295 elif request.method == "GET":
296 request.user.is_verified = True
298 return render_response(template_name,
300 context_instance = get_context(request,
303 @require_http_methods(["GET", "POST"])
304 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
306 Allows a user to create a local account.
308 In case of GET request renders a form for entering the user information.
309 In case of POST handles the signup.
311 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
312 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
313 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
314 (see activation_backends);
316 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
317 otherwise renders the same page with a success message.
319 On unsuccessful creation, renders ``template_name`` with an error message.
324 A custom template to render. This is optional;
325 if not specified, this will default to ``im/signup.html``.
328 A custom template to render in case of success. This is optional;
329 if not specified, this will default to ``im/signup_complete.html``.
332 An dictionary of variables to add to the template context.
336 im/signup.html or ``template_name`` keyword argument.
337 im/signup_complete.html or ``on_success`` keyword argument.
339 extra_context = extra_context or {}
340 if request.user.is_authenticated():
341 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
343 provider = get_query(request).get('provider', 'local')
344 id = get_query(request).get('id')
346 instance = AstakosUser.objects.get(id=id) if id else None
347 except AstakosUser.DoesNotExist:
352 backend = get_backend(request)
353 form = backend.get_signup_form(provider, instance)
355 form = SimpleBackend(request).get_signup_form(provider)
356 messages.add_message(request, messages.ERROR, e)
357 if request.method == 'POST':
359 user = form.save(commit=False)
361 result = backend.handle_activation(user)
362 status = messages.SUCCESS
363 message = result.message
365 if 'additional_email' in form.cleaned_data:
366 additional_email = form.cleaned_data['additional_email']
367 if additional_email != user.email:
368 user.additionalmail_set.create(email=additional_email)
369 msg = 'Additional email: %s saved for user %s.' % (
373 logger._log(LOGGING_LEVEL, msg, [])
374 if user and user.is_active:
375 next = request.POST.get('next', '')
376 return prepare_response(request, user, next=next)
377 messages.add_message(request, status, message)
378 return render_response(
380 context_instance=get_context(
385 except SendMailError, e:
387 status = messages.ERROR
389 messages.add_message(request, status, message)
390 except BaseException, e:
392 status = messages.ERROR
393 message = _('Something went wrong.')
394 messages.add_message(request, status, message)
396 return render_response(template_name,
399 context_instance=get_context(request, extra_context))
401 @require_http_methods(["GET", "POST"])
403 @signed_terms_required
404 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
406 Allows a user to send feedback.
408 In case of GET request renders a form for providing the feedback information.
409 In case of POST sends an email to support team.
411 If the user isn't logged in, redirects to settings.LOGIN_URL.
416 A custom template to use. This is optional; if not specified,
417 this will default to ``im/feedback.html``.
420 An dictionary of variables to add to the template context.
424 im/signup.html or ``template_name`` keyword argument.
428 * LOGIN_URL: login uri
429 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
431 extra_context = extra_context or {}
432 if request.method == 'GET':
433 form = FeedbackForm()
434 if request.method == 'POST':
436 return HttpResponse('Unauthorized', status=401)
438 form = FeedbackForm(request.POST)
440 msg = form.cleaned_data['feedback_msg']
441 data = form.cleaned_data['feedback_data']
443 send_feedback(msg, data, request.user, email_template_name)
444 except SendMailError, e:
446 status = messages.ERROR
448 message = _('Feedback successfully sent')
449 status = messages.SUCCESS
450 messages.add_message(request, status, message)
451 return render_response(template_name,
452 feedback_form = form,
453 context_instance = get_context(request, extra_context))
455 @require_http_methods(["GET"])
456 def logout(request, template='registration/logged_out.html', extra_context=None):
458 Wraps `django.contrib.auth.logout`.
460 extra_context = extra_context or {}
461 response = HttpResponse()
462 if request.user.is_authenticated():
463 email = request.user.email
465 next = restrict_next(
466 request.GET.get('next'),
470 response['Location'] = next
471 response.status_code = 302
473 response['Location'] = LOGOUT_NEXT
474 response.status_code = 301
476 messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
477 context = get_context(request, extra_context)
478 response.write(render_to_string(template, context_instance=context))
481 @require_http_methods(["GET", "POST"])
482 @transaction.commit_manually
483 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
485 Activates the user identified by the ``auth`` request parameter, sends a welcome email
486 and renews the user token.
488 The view uses commit_manually decorator in order to ensure the user state will be updated
489 only if the email will be send successfully.
491 token = request.GET.get('auth')
492 next = request.GET.get('next')
494 user = AstakosUser.objects.get(auth_token=token)
495 except AstakosUser.DoesNotExist:
496 return HttpResponseBadRequest(_('No such user'))
499 message = _('Account already active.')
500 messages.add_message(request, messages.ERROR, message)
501 return index(request)
504 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
505 response = prepare_response(request, user, next, renew=True)
508 except SendMailError, e:
510 messages.add_message(request, messages.ERROR, message)
511 transaction.rollback()
512 return index(request)
513 except BaseException, e:
514 status = messages.ERROR
515 message = _('Something went wrong.')
516 messages.add_message(request, messages.ERROR, message)
518 transaction.rollback()
519 return index(request)
521 @require_http_methods(["GET", "POST"])
522 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
523 extra_context = extra_context or {}
528 term = ApprovalTerms.objects.order_by('-id')[0]
533 term = ApprovalTerms.objects.get(id=term_id)
534 except ApprovalTermDoesNotExist, e:
538 return HttpResponseRedirect(reverse('astakos.im.views.index'))
539 f = open(term.location, 'r')
542 if request.method == 'POST':
543 next = restrict_next(
544 request.POST.get('next'),
548 next = reverse('astakos.im.views.index')
549 form = SignApprovalTermsForm(request.POST, instance=request.user)
550 if not form.is_valid():
551 return render_response(template_name,
553 approval_terms_form = form,
554 context_instance = get_context(request, extra_context))
556 return HttpResponseRedirect(next)
559 if request.user.is_authenticated() and not request.user.signed_terms():
560 form = SignApprovalTermsForm(instance=request.user)
561 return render_response(template_name,
563 approval_terms_form = form,
564 context_instance = get_context(request, extra_context))
566 @require_http_methods(["GET", "POST"])
568 @signed_terms_required
569 @transaction.commit_manually
570 def change_email(request, activation_key=None,
571 email_template_name='registration/email_change_email.txt',
572 form_template_name='registration/email_change_form.html',
573 confirm_template_name='registration/email_change_done.html',
575 extra_context = extra_context or {}
578 user = EmailChange.objects.change_email(activation_key)
579 if request.user.is_authenticated() and request.user == user:
580 msg = _('Email changed successfully.')
581 messages.add_message(request, messages.SUCCESS, msg)
583 response = prepare_response(request, user)
586 except ValueError, e:
587 messages.add_message(request, messages.ERROR, e)
588 return render_response(confirm_template_name,
589 modified_user = user if 'user' in locals() else None,
590 context_instance = get_context(request,
593 if not request.user.is_authenticated():
594 path = quote(request.get_full_path())
595 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
596 return HttpResponseRedirect(url + '?next=' + path)
597 form = EmailChangeForm(request.POST or None)
598 if request.method == 'POST' and form.is_valid():
600 ec = form.save(email_template_name, request)
601 except SendMailError, e:
602 status = messages.ERROR
604 transaction.rollback()
605 except IntegrityError, e:
606 status = messages.ERROR
607 msg = _('There is already a pending change email request.')
609 status = messages.SUCCESS
610 msg = _('Change email request has been registered succefully.\
611 You are going to receive a verification email in the new address.')
613 messages.add_message(request, status, msg)
614 return render_response(form_template_name,
618 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
619 extra_context = extra_context or {}
621 u = AstakosUser.objects.get(id=user_id)
622 except AstakosUser.DoesNotExist:
623 messages.error(request, _('Invalid user id'))
626 send_activation_func(u)
627 msg = _('Activation sent.')
628 messages.success(request, msg)
629 except SendMailError, e:
630 messages.error(request, e)
631 return render_response(
633 login_form = LoginForm(request=request),
634 context_instance = get_context(