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 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, activate as activate_func
64 from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL,
65 COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
68 logger = logging.getLogger(__name__)
70 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
72 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
73 keyword argument and returns an ``django.http.HttpResponse`` with the
77 tab = template.partition('_')[0].partition('.html')[0]
78 kwargs.setdefault('tab', tab)
79 html = render_to_string(template, kwargs, context_instance=context_instance)
80 response = HttpResponse(html, status=status)
82 set_cookie(response, context_instance['request'].user)
86 def requires_anonymous(func):
88 Decorator checkes whether the request.user is not Anonymous and in that case
89 redirects to `logout`.
92 def wrapper(request, *args):
93 if not request.user.is_anonymous():
94 next = urlencode({'next': request.build_absolute_uri()})
95 logout_uri = reverse(logout) + '?' + next
96 return HttpResponseRedirect(logout_uri)
97 return func(request, *args)
100 def signed_terms_required(func):
102 Decorator checkes whether the request.user is Anonymous and in that case
103 redirects to `logout`.
106 def wrapper(request, *args, **kwargs):
107 if request.user.is_authenticated() and not request.user.signed_terms():
108 params = urlencode({'next': request.build_absolute_uri(),
110 terms_uri = reverse('latest_terms') + '?' + params
111 return HttpResponseRedirect(terms_uri)
112 return func(request, *args, **kwargs)
115 @require_http_methods(["GET", "POST"])
116 @signed_terms_required
117 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
119 If there is logged on user renders the profile page otherwise renders login page.
123 ``login_template_name``
124 A custom login template to use. This is optional; if not specified,
125 this will default to ``im/login.html``.
127 ``profile_template_name``
128 A custom profile template to use. This is optional; if not specified,
129 this will default to ``im/profile.html``.
132 An dictionary of variables to add to the template context.
136 im/profile.html or im/login.html or ``template_name`` keyword argument.
139 template_name = login_template_name
140 if request.user.is_authenticated():
141 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
143 return render_response(
145 login_form = LoginForm(request=request),
146 context_instance = get_context(request, extra_context)
149 @require_http_methods(["GET", "POST"])
151 @signed_terms_required
152 @transaction.commit_manually
153 def invite(request, template_name='im/invitations.html', extra_context={}):
155 Allows a user to invite somebody else.
157 In case of GET request renders a form for providing the invitee information.
158 In case of POST checks whether the user has not run out of invitations and then
159 sends an invitation email to singup to the service.
161 The view uses commit_manually decorator in order to ensure the number of the
162 user invitations is going to be updated only if the email has been successfully sent.
164 If the user isn't logged in, redirects to settings.LOGIN_URL.
169 A custom template to use. This is optional; if not specified,
170 this will default to ``im/invitations.html``.
173 An dictionary of variables to add to the template context.
177 im/invitations.html or ``template_name`` keyword argument.
181 The view expectes the following settings are defined:
183 * LOGIN_URL: login uri
184 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
185 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
189 form = InvitationForm()
191 inviter = request.user
192 if request.method == 'POST':
193 form = InvitationForm(request.POST)
194 if inviter.invitations > 0:
197 invitation = form.save()
198 invite_func(invitation, inviter)
199 status = messages.SUCCESS
200 message = _('Invitation sent to %s' % invitation.username)
201 except SendMailError, e:
202 status = messages.ERROR
204 transaction.rollback()
205 except BaseException, e:
206 status = messages.ERROR
207 message = _('Something went wrong.')
209 transaction.rollback()
213 status = messages.ERROR
214 message = _('No invitations left')
215 messages.add_message(request, status, message)
217 sent = [{'email': inv.username,
218 'realname': inv.realname,
219 'is_consumed': inv.is_consumed}
220 for inv in request.user.invitations_sent.all()]
221 kwargs = {'inviter': inviter,
223 context = get_context(request, extra_context, **kwargs)
224 return render_response(template_name,
225 invitation_form = form,
226 context_instance = context)
228 @require_http_methods(["GET", "POST"])
230 @signed_terms_required
231 def edit_profile(request, template_name='im/profile.html', extra_context={}):
233 Allows a user to edit his/her profile.
235 In case of GET request renders a form for displaying the user information.
236 In case of POST updates the user informantion and redirects to ``next``
237 url parameter if exists.
239 If the user isn't logged in, redirects to settings.LOGIN_URL.
244 A custom template to use. This is optional; if not specified,
245 this will default to ``im/profile.html``.
248 An dictionary of variables to add to the template context.
252 im/profile.html or ``template_name`` keyword argument.
256 The view expectes the following settings are defined:
258 * LOGIN_URL: login uri
260 form = ProfileForm(instance=request.user)
261 extra_context['next'] = request.GET.get('next')
263 if request.method == 'POST':
264 form = ProfileForm(request.POST, instance=request.user)
267 prev_token = request.user.auth_token
269 reset_cookie = user.auth_token != prev_token
270 form = ProfileForm(instance=user)
271 next = request.POST.get('next')
273 return redirect(next)
274 msg = _('<p>Profile has been updated successfully</p>')
275 messages.add_message(request, messages.SUCCESS, msg)
276 except ValueError, ve:
277 messages.add_message(request, messages.ERROR, ve)
278 elif request.method == "GET":
279 request.user.is_verified = True
281 return render_response(template_name,
282 reset_cookie = reset_cookie,
284 context_instance = get_context(request,
287 @require_http_methods(["GET", "POST"])
288 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
290 Allows a user to create a local account.
292 In case of GET request renders a form for entering the user information.
293 In case of POST handles the signup.
295 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
296 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
297 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
298 (see activation_backends);
300 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
301 otherwise renders the same page with a success message.
303 On unsuccessful creation, renders ``template_name`` with an error message.
308 A custom template to render. This is optional;
309 if not specified, this will default to ``im/signup.html``.
312 A custom template to render in case of success. This is optional;
313 if not specified, this will default to ``im/signup_complete.html``.
316 An dictionary of variables to add to the template context.
320 im/signup.html or ``template_name`` keyword argument.
321 im/signup_complete.html or ``on_success`` keyword argument.
323 if request.user.is_authenticated():
324 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
326 provider = get_query(request).get('provider', 'local')
329 backend = get_backend(request)
330 form = backend.get_signup_form(provider)
332 form = SimpleBackend(request).get_signup_form(provider)
333 messages.add_message(request, messages.ERROR, e)
334 if request.method == 'POST':
336 user = form.save(commit=False)
338 result = backend.handle_activation(user)
339 status = messages.SUCCESS
340 message = result.message
342 if 'additional_email' in form.cleaned_data:
343 additional_email = form.cleaned_data['additional_email']
344 if additional_email != user.email:
345 user.additionalmail_set.create(email=additional_email)
346 msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
347 logger._log(LOGGING_LEVEL, msg, [])
348 if user and user.is_active:
349 next = request.POST.get('next', '')
350 return prepare_response(request, user, next=next)
351 messages.add_message(request, status, message)
352 return render_response(on_success,
353 context_instance=get_context(request, extra_context))
354 except SendMailError, e:
355 status = messages.ERROR
357 messages.add_message(request, status, message)
358 except BaseException, e:
359 status = messages.ERROR
360 message = _('Something went wrong.')
361 messages.add_message(request, status, message)
363 return render_response(template_name,
366 context_instance=get_context(request, extra_context))
368 @require_http_methods(["GET", "POST"])
370 @signed_terms_required
371 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
373 Allows a user to send feedback.
375 In case of GET request renders a form for providing the feedback information.
376 In case of POST sends an email to support team.
378 If the user isn't logged in, redirects to settings.LOGIN_URL.
383 A custom template to use. This is optional; if not specified,
384 this will default to ``im/feedback.html``.
387 An dictionary of variables to add to the template context.
391 im/signup.html or ``template_name`` keyword argument.
395 * LOGIN_URL: login uri
396 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
398 if request.method == 'GET':
399 form = FeedbackForm()
400 if request.method == 'POST':
402 return HttpResponse('Unauthorized', status=401)
404 form = FeedbackForm(request.POST)
406 msg = form.cleaned_data['feedback_msg']
407 data = form.cleaned_data['feedback_data']
409 send_feedback(msg, data, request.user, email_template_name)
410 except SendMailError, e:
412 status = messages.ERROR
414 message = _('Feedback successfully sent')
415 status = messages.SUCCESS
416 messages.add_message(request, status, message)
417 return render_response(template_name,
418 feedback_form = form,
419 context_instance = get_context(request, extra_context))
421 @require_http_methods(["GET", "POST"])
422 def logout(request, template='registration/logged_out.html', extra_context={}):
424 Wraps `django.contrib.auth.logout` and delete the cookie.
426 response = HttpResponse()
427 if request.user.is_authenticated():
428 email = request.user.email
430 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
431 msg = 'Cookie deleted for %s' % email
432 logger._log(LOGGING_LEVEL, msg, [])
433 next = request.GET.get('next')
435 response['Location'] = next
436 response.status_code = 302
439 response['Location'] = LOGOUT_NEXT
440 response.status_code = 301
442 messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
443 context = get_context(request, extra_context)
444 response.write(render_to_string(template, context_instance=context))
447 @require_http_methods(["GET", "POST"])
448 @transaction.commit_manually
449 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
451 Activates the user identified by the ``auth`` request parameter, sends a welcome email
452 and renews the user token.
454 The view uses commit_manually decorator in order to ensure the user state will be updated
455 only if the email will be send successfully.
457 token = request.GET.get('auth')
458 next = request.GET.get('next')
460 user = AstakosUser.objects.get(auth_token=token)
461 except AstakosUser.DoesNotExist:
462 return HttpResponseBadRequest(_('No such user'))
465 message = _('Account already active.')
466 messages.add_message(request, messages.ERROR, message)
467 return index(request)
470 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
471 response = prepare_response(request, user, next, renew=True)
474 except SendMailError, e:
476 messages.add_message(request, messages.ERROR, message)
477 transaction.rollback()
478 return index(request)
479 except BaseException, e:
480 status = messages.ERROR
481 message = _('Something went wrong.')
482 messages.add_message(request, messages.ERROR, message)
484 transaction.rollback()
485 return index(request)
487 @require_http_methods(["GET", "POST"])
488 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
493 term = ApprovalTerms.objects.order_by('-id')[0]
498 term = ApprovalTerms.objects.get(id=term_id)
499 except ApprovalTermDoesNotExist, e:
503 return HttpResponseRedirect(reverse('astakos.im.views.index'))
504 f = open(term.location, 'r')
507 if request.method == 'POST':
508 next = request.POST.get('next')
510 next = reverse('astakos.im.views.index')
511 form = SignApprovalTermsForm(request.POST, instance=request.user)
512 if not form.is_valid():
513 return render_response(template_name,
515 approval_terms_form = form,
516 context_instance = get_context(request, extra_context))
518 return HttpResponseRedirect(next)
521 if request.user.is_authenticated() and not request.user.signed_terms():
522 form = SignApprovalTermsForm(instance=request.user)
523 return render_response(template_name,
525 approval_terms_form = form,
526 context_instance = get_context(request, extra_context))
528 @require_http_methods(["GET", "POST"])
529 @signed_terms_required
530 def change_password(request):
531 return password_change(request,
532 post_change_redirect=reverse('astakos.im.views.edit_profile'),
533 password_change_form=ExtendedPasswordChangeForm)
535 @require_http_methods(["GET", "POST"])
537 @signed_terms_required
538 @transaction.commit_manually
539 def change_email(request, activation_key=None,
540 email_template_name='registration/email_change_email.txt',
541 form_template_name='registration/email_change_form.html',
542 confirm_template_name='registration/email_change_done.html',
546 user = EmailChange.objects.change_email(activation_key)
547 if request.user.is_authenticated() and request.user == user:
548 msg = _('Email changed successfully.')
549 messages.add_message(request, messages.SUCCESS, msg)
551 response = prepare_response(request, user)
554 except ValueError, e:
555 messages.add_message(request, messages.ERROR, e)
556 return render_response(confirm_template_name,
557 modified_user = user if 'user' in locals() else None,
558 context_instance = get_context(request,
561 if not request.user.is_authenticated():
562 path = quote(request.get_full_path())
563 url = request.build_absolute_uri(reverse('astakos.im.views.index'))
564 return HttpResponseRedirect(url + '?next=' + path)
565 form = EmailChangeForm(request.POST or None)
566 if request.method == 'POST' and form.is_valid():
568 ec = form.save(email_template_name, request)
569 except SendMailError, e:
570 status = messages.ERROR
572 transaction.rollback()
573 except IntegrityError, e:
574 status = messages.ERROR
575 msg = _('There is already a pending change email request.')
577 status = messages.SUCCESS
578 msg = _('Change email request has been registered succefully.\
579 You are going to receive a verification email in the new address.')
581 messages.add_message(request, status, msg)
582 return render_response(form_template_name,
584 context_instance = get_context(request,