1 from django import forms
2 from django.conf.urls.defaults import url, patterns, include
3 from django.contrib import messages
4 from django.contrib.auth import authenticate, login, views as auth_views
5 from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
6 from django.contrib.auth.models import User
7 from django.contrib.auth.tokens import default_token_generator as password_token_generator
8 from django.contrib.sites.models import Site
9 from django.core.mail import EmailMultiAlternatives, send_mail
10 from django.db import models
11 from django.http import Http404, HttpResponseRedirect
12 from django.shortcuts import render_to_response, get_object_or_404
13 from django.template.defaultfilters import striptags
14 from django.utils.http import int_to_base36, base36_to_int
15 from django.utils.translation import ugettext as _
16 from django.views.decorators.cache import never_cache
17 from django.views.decorators.csrf import csrf_protect
18 from philo.models import MultiView, Page
19 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
20 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
24 class LoginMultiView(MultiView):
26 Handles exclusively methods and views related to logging users in and out.
28 login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
29 login_form = WaldoAuthenticationForm
32 def urlpatterns(self):
34 url(r'^login$', self.login, name='login'),
35 url(r'^logout$', self.logout, name='logout'),
38 def set_requirement_redirect(self, request, redirect=None):
39 "Figure out where someone should end up after landing on a `requirement` page like the login page."
40 if redirect is not None:
42 elif 'requirement_redirect' in request.session:
45 referrer = request.META.get('HTTP_REFERER', None)
47 if referrer is not None:
48 referrer = urlparse.urlparse(referrer)
50 if host != request.get_host():
53 redirect = '%s?%s' % (referrer[2], referrer[4])
55 path = request.get_full_path()
56 if referrer is None or redirect == path:
57 # Default to the index page if we can't find a referrer or
58 # if we'd otherwise redirect to where we already are.
59 redirect = request.node.get_absolute_url()
61 request.session['requirement_redirect'] = redirect
63 def get_requirement_redirect(self, request, default=None):
64 redirect = request.session.pop('requirement_redirect', None)
65 # Security checks a la django.contrib.auth.views.login
66 if not redirect or ' ' in redirect:
69 netloc = urlparse.urlparse(redirect)[1]
70 if netloc and netloc != request.get_host():
73 redirect = request.node.get_absolute_url()
77 def login(self, request, extra_context=None):
79 Displays the login form for the given HttpRequest.
81 self.set_requirement_redirect(request)
83 # Redirect already-authenticated users to the index page.
84 if request.user.is_authenticated():
85 messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
86 return HttpResponseRedirect(self.get_requirement_redirect(request))
88 if request.method == 'POST':
89 form = self.login_form(request=request, data=request.POST)
91 redirect = self.get_requirement_redirect(request)
92 login(request, form.get_user())
94 if request.session.test_cookie_worked():
95 request.session.delete_test_cookie()
97 return HttpResponseRedirect(redirect)
99 form = self.login_form()
101 request.session.set_test_cookie()
103 context = self.get_context()
104 context.update(extra_context or {})
108 return self.login_page.render_to_response(request, extra_context=context)
111 def logout(self, request, extra_context=None):
112 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
114 def login_required(self, view):
115 def inner(request, *args, **kwargs):
116 if not request.user.is_authenticated():
117 self.set_requirement_redirect(request, redirect=request.path)
119 messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
120 return HttpResponseRedirect(self.reverse('login', node=request.node))
121 return view(request, *args, **kwargs)
129 class PasswordMultiView(LoginMultiView):
130 "Adds on views for password-related functions."
131 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
132 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
133 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
134 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
137 def urlpatterns(self):
138 urlpatterns = super(PasswordMultiView, self).urlpatterns
140 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
141 urlpatterns += patterns('',
142 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
143 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
146 if self.password_change_page:
147 urlpatterns += patterns('',
148 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
152 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
153 token = token_generator.make_token(user, *(token_args or []))
155 'uidb36': int_to_base36(user.id),
158 kwargs.update(reverse_kwargs or {})
159 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
161 def send_confirmation_email(self, subject, email, page, extra_context):
162 text_content = page.render_to_string(extra_context=extra_context)
163 from_email = 'noreply@%s' % Site.objects.get_current().domain
165 if page.template.mimetype == 'text/html':
166 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
167 msg.attach_alternative(text_content, 'text/html')
170 send_mail(subject, text_content, from_email, [email])
172 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
173 if request.user.is_authenticated():
174 return HttpResponseRedirect(request.node.get_absolute_url())
176 if request.method == 'POST':
177 form = PasswordResetForm(request.POST)
179 current_site = Site.objects.get_current()
180 for user in form.users_cache:
182 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
183 'username': user.username
185 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
186 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)
187 return HttpResponseRedirect('')
189 form = PasswordResetForm()
191 context = self.get_context()
192 context.update(extra_context or {})
196 return self.password_reset_page.render_to_response(request, extra_context=context)
198 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
200 Checks that a given hash in a password reset link is valid. If so,
201 displays the password set form.
203 assert uidb36 is not None and token is not None
205 uid_int = base36_to_int(uidb36)
209 user = get_object_or_404(User, id=uid_int)
211 if token_generator.check_token(user, token):
212 if request.method == 'POST':
213 form = SetPasswordForm(user, request.POST)
217 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
218 return HttpResponseRedirect(self.reverse('login', node=request.node))
220 form = SetPasswordForm(user)
222 context = self.get_context()
223 context.update(extra_context or {})
227 return self.password_set_page.render_to_response(request, extra_context=context)
231 def password_change(self, request, extra_context=None):
232 if request.method == 'POST':
233 form = PasswordChangeForm(request.user, request.POST)
236 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
237 return HttpResponseRedirect('')
239 form = PasswordChangeForm(request.user)
241 context = self.get_context()
242 context.update(extra_context or {})
246 return self.password_change_page.render_to_response(request, extra_context=context)
252 class RegistrationMultiView(PasswordMultiView):
253 """Adds on the pages necessary for letting new users register."""
254 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
255 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
256 registration_form = RegistrationForm
259 def urlpatterns(self):
260 urlpatterns = super(RegistrationMultiView, self).urlpatterns
261 if self.register_page and self.register_confirmation_email:
262 urlpatterns += patterns('',
263 url(r'^register$', csrf_protect(self.register), name='register'),
264 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
268 def register(self, request, extra_context=None, token_generator=registration_token_generator):
269 if request.user.is_authenticated():
270 return HttpResponseRedirect(request.node.get_absolute_url())
272 if request.method == 'POST':
273 form = self.registration_form(request.POST)
277 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
279 current_site = Site.objects.get_current()
280 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
281 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
282 return HttpResponseRedirect(request.node.get_absolute_url())
284 form = self.registration_form()
286 context = self.get_context()
287 context.update(extra_context or {})
291 return self.register_page.render_to_response(request, extra_context=context)
293 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
295 Checks that a given hash in a registration link is valid and activates
296 the given account. If so, log them in and redirect to
297 self.post_register_confirm_redirect.
299 assert uidb36 is not None and token is not None
301 uid_int = base36_to_int(uidb36)
305 user = get_object_or_404(User, id=uid_int)
306 if token_generator.check_token(user, token):
307 user.is_active = True
308 true_password = user.password
309 temp_password = token_generator.make_token(user)
311 user.set_password(temp_password)
313 authenticated_user = authenticate(username=user.username, password=temp_password)
314 login(request, authenticated_user)
316 # if anything goes wrong, do our best make sure that the true password is restored.
317 user.password = true_password
319 return self.post_register_confirm_redirect(request)
323 def post_register_confirm_redirect(self, request):
324 return HttpResponseRedirect(request.node.get_absolute_url())
330 class AccountMultiView(RegistrationMultiView):
332 By default, the `account` consists of the first_name, last_name, and email fields
333 of the User model. Using a different account model is as simple as writing a form that
334 accepts a User instance as the first argument.
336 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
337 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.")
339 account_form = UserAccountForm
342 def urlpatterns(self):
343 urlpatterns = super(AccountMultiView, self).urlpatterns
344 if self.manage_account_page:
345 urlpatterns += patterns('',
346 url(r'^account$', self.login_required(self.account_view), name='account'),
348 if self.email_change_confirmation_email:
349 urlpatterns += patterns('',
350 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
354 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
355 if request.method == 'POST':
356 form = self.account_form(request.user, request.POST, request.FILES)
359 message = "Account information saved."
360 redirect = self.get_requirement_redirect(request, default='')
361 if 'email' in form.changed_data and self.email_change_confirmation_email:
362 # ModelForms modify their instances in-place during
363 # validation, so reset the instance's email to its
364 # previous value here, then remove the new value
365 # from cleaned_data. We only do this if an email
366 # change confirmation email is available.
367 request.user.email = form.initial['email']
369 email = form.cleaned_data.pop('email')
372 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
374 current_site = Site.objects.get_current()
375 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
377 message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
378 if not request.user.email:
379 message += " You will need to confirm the email before accessing pages that require a valid account."
385 message += " Here you go!"
387 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
388 return HttpResponseRedirect(redirect)
390 form = self.account_form(request.user)
392 context = self.get_context()
393 context.update(extra_context or {})
397 return self.manage_account_page.render_to_response(request, extra_context=context)
399 def has_valid_account(self, user):
400 form = self.account_form(user, {})
401 form.data = form.initial
402 return form.is_valid()
404 def account_required(self, view):
405 def inner(request, *args, **kwargs):
406 if not self.has_valid_account(request.user):
407 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
408 if self.manage_account_page:
409 self.set_requirement_redirect(request, redirect=request.path)
410 redirect = self.reverse('account', node=request.node)
412 redirect = node.get_absolute_url()
413 return HttpResponseRedirect(redirect)
414 return view(request, *args, **kwargs)
416 inner = self.login_required(inner)
419 def post_register_confirm_redirect(self, request):
420 if self.manage_account_page:
421 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
422 return HttpResponseRedirect(self.reverse('account', node=request.node))
423 return super(AccountMultiView, self).post_register_confirm_redirect(request)
425 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
427 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
429 assert uidb36 is not None and token is not None and email is not None
432 uid_int = base36_to_int(uidb36)
436 user = get_object_or_404(User, id=uid_int)
438 email = '@'.join(email.rsplit('+', 1))
440 if email == user.email:
441 # Then short-circuit.
444 if token_generator.check_token(user, email, token):
447 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
448 if self.manage_account_page:
449 redirect = self.reverse('account', node=request.node)
451 redirect = request.node.get_absolute_url()
452 return HttpResponseRedirect(redirect)