3 from django import forms
4 from django.conf.urls.defaults import url, patterns, include
5 from django.contrib import messages
6 from django.contrib.auth import authenticate, login, views as auth_views
7 from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
8 from django.contrib.auth.models import User
9 from django.contrib.auth.tokens import default_token_generator as password_token_generator
10 from django.contrib.sites.models import Site
11 from django.core.mail import EmailMultiAlternatives, send_mail
12 from django.db import models
13 from django.http import Http404, HttpResponseRedirect
14 from django.shortcuts import render_to_response, get_object_or_404
15 from django.template.defaultfilters import striptags
16 from django.utils.http import int_to_base36, base36_to_int
17 from django.utils.translation import ugettext as _
18 from django.views.decorators.cache import never_cache
19 from django.views.decorators.csrf import csrf_protect
21 from philo.models import MultiView, Page
22 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
23 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
26 class LoginMultiView(MultiView):
28 Handles exclusively methods and views related to logging users in and out.
30 login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
31 login_form = WaldoAuthenticationForm
34 def urlpatterns(self):
36 url(r'^login$', self.login, name='login'),
37 url(r'^logout$', self.logout, name='logout'),
40 def set_requirement_redirect(self, request, redirect=None):
41 "Figure out where someone should end up after landing on a `requirement` page like the login page."
42 if redirect is not None:
44 elif 'requirement_redirect' in request.session:
47 referrer = request.META.get('HTTP_REFERER', None)
49 if referrer is not None:
50 referrer = urlparse.urlparse(referrer)
52 if host != request.get_host():
55 redirect = '%s?%s' % (referrer[2], referrer[4])
57 path = request.get_full_path()
58 if referrer is None or redirect == path:
59 # Default to the index page if we can't find a referrer or
60 # if we'd otherwise redirect to where we already are.
61 redirect = request.node.get_absolute_url()
63 request.session['requirement_redirect'] = redirect
65 def get_requirement_redirect(self, request, default=None):
66 redirect = request.session.pop('requirement_redirect', None)
67 # Security checks a la django.contrib.auth.views.login
68 if not redirect or ' ' in redirect:
71 netloc = urlparse.urlparse(redirect)[1]
72 if netloc and netloc != request.get_host():
75 redirect = request.node.get_absolute_url()
79 def login(self, request, extra_context=None):
81 Displays the login form for the given HttpRequest.
83 self.set_requirement_redirect(request)
85 # Redirect already-authenticated users to the index page.
86 if request.user.is_authenticated():
87 messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
88 return HttpResponseRedirect(self.get_requirement_redirect(request))
90 if request.method == 'POST':
91 form = self.login_form(request=request, data=request.POST)
93 redirect = self.get_requirement_redirect(request)
94 login(request, form.get_user())
96 if request.session.test_cookie_worked():
97 request.session.delete_test_cookie()
99 return HttpResponseRedirect(redirect)
101 form = self.login_form()
103 request.session.set_test_cookie()
105 context = self.get_context()
106 context.update(extra_context or {})
110 return self.login_page.render_to_response(request, extra_context=context)
113 def logout(self, request, extra_context=None):
114 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
116 def login_required(self, view):
117 def inner(request, *args, **kwargs):
118 if not request.user.is_authenticated():
119 self.set_requirement_redirect(request, redirect=request.path)
121 messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
122 return HttpResponseRedirect(self.reverse('login', node=request.node))
123 return view(request, *args, **kwargs)
131 class PasswordMultiView(LoginMultiView):
132 "Adds on views for password-related functions."
133 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
134 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
135 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
136 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
138 password_change_form = PasswordChangeForm
139 password_set_form = SetPasswordForm
140 password_reset_form = PasswordResetForm
143 def urlpatterns(self):
144 urlpatterns = super(PasswordMultiView, self).urlpatterns
146 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
147 urlpatterns += patterns('',
148 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
149 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
152 if self.password_change_page:
153 urlpatterns += patterns('',
154 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
158 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
159 token = token_generator.make_token(user, *(token_args or []))
161 'uidb36': int_to_base36(user.id),
164 kwargs.update(reverse_kwargs or {})
165 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
167 def send_confirmation_email(self, subject, email, page, extra_context):
168 text_content = page.render_to_string(extra_context=extra_context)
169 from_email = 'noreply@%s' % Site.objects.get_current().domain
171 if page.template.mimetype == 'text/html':
172 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
173 msg.attach_alternative(text_content, 'text/html')
176 send_mail(subject, text_content, from_email, [email])
178 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
179 if request.user.is_authenticated():
180 return HttpResponseRedirect(request.node.get_absolute_url())
182 if request.method == 'POST':
183 form = self.password_reset_form(request.POST)
185 current_site = Site.objects.get_current()
186 for user in form.users_cache:
188 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
190 'site': current_site,
193 # Deprecated... leave in for backwards-compatibility
194 'username': user.username
196 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
197 messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
198 return HttpResponseRedirect('')
200 form = self.password_reset_form()
202 context = self.get_context()
203 context.update(extra_context or {})
207 return self.password_reset_page.render_to_response(request, extra_context=context)
209 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
211 Checks that a given hash in a password reset link is valid. If so,
212 displays the password set form.
214 assert uidb36 is not None and token is not None
216 uid_int = base36_to_int(uidb36)
220 user = get_object_or_404(User, id=uid_int)
222 if token_generator.check_token(user, token):
223 if request.method == 'POST':
224 form = self.password_set_form(user, request.POST)
228 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
229 return HttpResponseRedirect(self.reverse('login', node=request.node))
231 form = self.password_set_form(user)
233 context = self.get_context()
234 context.update(extra_context or {})
238 return self.password_set_page.render_to_response(request, extra_context=context)
242 def password_change(self, request, extra_context=None):
243 if request.method == 'POST':
244 form = self.password_change_form(request.user, request.POST)
247 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
248 return HttpResponseRedirect('')
250 form = self.password_change_form(request.user)
252 context = self.get_context()
253 context.update(extra_context or {})
257 return self.password_change_page.render_to_response(request, extra_context=context)
263 class RegistrationMultiView(PasswordMultiView):
264 """Adds on the pages necessary for letting new users register."""
265 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
266 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
267 registration_form = RegistrationForm
270 def urlpatterns(self):
271 urlpatterns = super(RegistrationMultiView, self).urlpatterns
272 if self.register_page and self.register_confirmation_email:
273 urlpatterns += patterns('',
274 url(r'^register$', csrf_protect(self.register), name='register'),
275 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
279 def register(self, request, extra_context=None, token_generator=registration_token_generator):
280 if request.user.is_authenticated():
281 return HttpResponseRedirect(request.node.get_absolute_url())
283 if request.method == 'POST':
284 form = self.registration_form(request.POST)
287 current_site = Site.objects.get_current()
289 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node, secure=request.is_secure()),
291 'site': current_site,
294 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
295 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
296 return HttpResponseRedirect(request.node.get_absolute_url())
298 form = self.registration_form()
300 context = self.get_context()
301 context.update(extra_context or {})
305 return self.register_page.render_to_response(request, extra_context=context)
307 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
309 Checks that a given hash in a registration link is valid and activates
310 the given account. If so, log them in and redirect to
311 self.post_register_confirm_redirect.
313 assert uidb36 is not None and token is not None
315 uid_int = base36_to_int(uidb36)
319 user = get_object_or_404(User, id=uid_int)
320 if token_generator.check_token(user, token):
321 user.is_active = True
322 true_password = user.password
323 temp_password = token_generator.make_token(user)
325 user.set_password(temp_password)
327 authenticated_user = authenticate(username=user.username, password=temp_password)
328 login(request, authenticated_user)
330 # if anything goes wrong, do our best make sure that the true password is restored.
331 user.password = true_password
333 return self.post_register_confirm_redirect(request)
337 def post_register_confirm_redirect(self, request):
338 return HttpResponseRedirect(request.node.get_absolute_url())
344 class AccountMultiView(RegistrationMultiView):
346 By default, the `account` consists of the first_name, last_name, and email fields
347 of the User model. Using a different account model is as simple as writing a form that
348 accepts a User instance as the first argument.
350 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
351 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
353 account_form = UserAccountForm
356 def urlpatterns(self):
357 urlpatterns = super(AccountMultiView, self).urlpatterns
358 if self.manage_account_page:
359 urlpatterns += patterns('',
360 url(r'^account$', self.login_required(self.account_view), name='account'),
362 if self.email_change_confirmation_email:
363 urlpatterns += patterns('',
364 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
368 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
369 if request.method == 'POST':
370 form = self.account_form(request.user, request.POST, request.FILES)
373 message = "Account information saved."
374 redirect = self.get_requirement_redirect(request, default='')
375 if 'email' in form.changed_data and self.email_change_confirmation_email:
376 # ModelForms modify their instances in-place during
377 # validation, so reset the instance's email to its
378 # previous value here, then remove the new value
379 # from cleaned_data. We only do this if an email
380 # change confirmation email is available.
381 request.user.email = form.initial['email']
383 email = form.cleaned_data.pop('email')
385 current_site = Site.objects.get_current()
388 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')}, secure=request.is_secure()),
389 'user': request.user,
390 'site': current_site,
393 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
395 message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
396 if not request.user.email:
397 message += " You will need to confirm the email before accessing pages that require a valid account."
403 message += " Here you go!"
405 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
406 return HttpResponseRedirect(redirect)
408 form = self.account_form(request.user)
410 context = self.get_context()
411 context.update(extra_context or {})
415 return self.manage_account_page.render_to_response(request, extra_context=context)
417 def has_valid_account(self, user):
418 form = self.account_form(user, {})
419 form.data = form.initial
420 return form.is_valid()
422 def account_required(self, view):
423 def inner(request, *args, **kwargs):
424 if not self.has_valid_account(request.user):
425 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
426 if self.manage_account_page:
427 self.set_requirement_redirect(request, redirect=request.path)
428 redirect = self.reverse('account', node=request.node)
430 redirect = node.get_absolute_url()
431 return HttpResponseRedirect(redirect)
432 return view(request, *args, **kwargs)
434 inner = self.login_required(inner)
437 def post_register_confirm_redirect(self, request):
438 if self.manage_account_page:
439 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
440 return HttpResponseRedirect(self.reverse('account', node=request.node))
441 return super(AccountMultiView, self).post_register_confirm_redirect(request)
443 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
445 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
447 assert uidb36 is not None and token is not None and email is not None
450 uid_int = base36_to_int(uidb36)
454 user = get_object_or_404(User, id=uid_int)
456 email = '@'.join(email.rsplit('+', 1))
458 if email == user.email:
459 # Then short-circuit.
462 if token_generator.check_token(user, email, token):
465 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
466 if self.manage_account_page:
467 redirect = self.reverse('account', node=request.node)
469 redirect = request.node.get_absolute_url()
470 return HttpResponseRedirect(redirect)