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 login, registration, and forgotten passwords. In other words, this
27 multiview provides exclusively view and methods related to usernames and
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 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
133 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
134 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
135 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
138 def urlpatterns(self):
139 urlpatterns = super(PasswordMultiView, self).urlpatterns
141 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
142 urlpatterns += patterns('',
143 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
144 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
147 if self.password_change_page:
148 urlpatterns += patterns('',
149 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
153 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
154 token = token_generator.make_token(user, *(token_args or []))
156 'uidb36': int_to_base36(user.id),
159 kwargs.update(reverse_kwargs or {})
160 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
162 def send_confirmation_email(self, subject, email, page, extra_context):
163 text_content = page.render_to_string(extra_context=extra_context)
164 from_email = 'noreply@%s' % Site.objects.get_current().domain
166 if page.template.mimetype == 'text/html':
167 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
168 msg.attach_alternative(text_content, 'text/html')
171 send_mail(subject, text_content, from_email, [email])
173 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
174 if request.user.is_authenticated():
175 return HttpResponseRedirect(request.node.get_absolute_url())
177 if request.method == 'POST':
178 form = PasswordResetForm(request.POST)
180 current_site = Site.objects.get_current()
181 for user in form.users_cache:
183 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
184 'username': user.username
186 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
187 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)
188 return HttpResponseRedirect('')
190 form = PasswordResetForm()
192 context = self.get_context()
193 context.update(extra_context or {})
197 return self.password_reset_page.render_to_response(request, extra_context=context)
199 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
201 Checks that a given hash in a password reset link is valid. If so,
202 displays the password set form.
204 assert uidb36 is not None and token is not None
206 uid_int = base36_to_int(uidb36)
210 user = get_object_or_404(User, id=uid_int)
212 if token_generator.check_token(user, token):
213 if request.method == 'POST':
214 form = SetPasswordForm(user, request.POST)
218 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
219 return HttpResponseRedirect(self.reverse('login', node=request.node))
221 form = SetPasswordForm(user)
223 context = self.get_context()
224 context.update(extra_context or {})
228 return self.password_set_page.render_to_response(request, extra_context=context)
232 def password_change(self, request, extra_context=None):
233 if request.method == 'POST':
234 form = PasswordChangeForm(request.user, request.POST)
237 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
238 return HttpResponseRedirect('')
240 form = PasswordChangeForm(request.user)
242 context = self.get_context()
243 context.update(extra_context or {})
247 return self.password_change_page.render_to_response(request, extra_context=context)
253 class RegistrationMultiView(PasswordMultiView):
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)
258 def urlpatterns(self):
259 urlpatterns = super(RegistrationMultiView, self).urlpatterns
260 if self.register_page and self.register_confirmation_email:
261 urlpatterns += patterns('',
262 url(r'^register$', csrf_protect(self.register), name='register'),
263 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
267 def register(self, request, extra_context=None, token_generator=registration_token_generator):
268 if request.user.is_authenticated():
269 return HttpResponseRedirect(request.node.get_absolute_url())
271 if request.method == 'POST':
272 form = RegistrationForm(request.POST)
276 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
278 current_site = Site.objects.get_current()
279 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
280 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
281 return HttpResponseRedirect(request.node.get_absolute_url())
283 form = RegistrationForm()
285 context = self.get_context()
286 context.update(extra_context or {})
290 return self.register_page.render_to_response(request, extra_context=context)
292 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
294 Checks that a given hash in a registration link is valid and activates
295 the given account. If so, log them in and redirect to
296 self.post_register_confirm_redirect.
298 assert uidb36 is not None and token is not None
300 uid_int = base36_to_int(uidb36)
304 user = get_object_or_404(User, id=uid_int)
305 if token_generator.check_token(user, token):
306 user.is_active = True
307 true_password = user.password
308 temp_password = token_generator.make_token(user)
310 user.set_password(temp_password)
312 authenticated_user = authenticate(username=user.username, password=temp_password)
313 login(request, authenticated_user)
315 # if anything goes wrong, do our best make sure that the true password is restored.
316 user.password = true_password
318 return self.post_register_confirm_redirect(request)
322 def post_register_confirm_redirect(self, request):
323 return HttpResponseRedirect(request.node.get_absolute_url())
329 class AccountMultiView(RegistrationMultiView):
331 By default, the `account` consists of the first_name, last_name, and email fields
332 of the User model. Using a different account model is as simple as writing a form that
333 accepts a User instance as the first argument.
335 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
336 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.")
338 account_form = UserAccountForm
341 def urlpatterns(self):
342 urlpatterns = super(AccountMultiView, self).urlpatterns
343 if self.manage_account_page:
344 urlpatterns += patterns('',
345 url(r'^account$', self.login_required(self.account_view), name='account'),
347 if self.email_change_confirmation_email:
348 urlpatterns += patterns('',
349 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
353 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
354 if request.method == 'POST':
355 form = self.account_form(request.user, request.POST, request.FILES)
358 message = "Account information saved."
359 redirect = self.get_requirement_redirect(request, default='')
360 if 'email' in form.changed_data and self.email_change_confirmation_email:
361 # ModelForms modify their instances in-place during
362 # validation, so reset the instance's email to its
363 # previous value here, then remove the new value
364 # from cleaned_data. We only do this if an email
365 # change confirmation email is available.
366 request.user.email = form.initial['email']
368 email = form.cleaned_data.pop('email')
371 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
373 current_site = Site.objects.get_current()
374 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
376 message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
377 if not request.user.email:
378 message += " You will need to confirm the email before accessing pages that require a valid account."
384 message += " Here you go!"
386 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
387 return HttpResponseRedirect(redirect)
389 form = self.account_form(request.user)
391 context = self.get_context()
392 context.update(extra_context or {})
396 return self.manage_account_page.render_to_response(request, extra_context=context)
398 def has_valid_account(self, user):
399 form = self.account_form(user, {})
400 form.data = form.initial
401 return form.is_valid()
403 def account_required(self, view):
404 def inner(request, *args, **kwargs):
405 if not self.has_valid_account(request.user):
406 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
407 if self.manage_account_page:
408 self.set_requirement_redirect(request, redirect=request.path)
409 redirect = self.reverse('account', node=request.node)
411 redirect = node.get_absolute_url()
412 return HttpResponseRedirect(redirect)
413 return view(request, *args, **kwargs)
415 inner = self.login_required(inner)
418 def post_register_confirm_redirect(self, request):
419 if self.manage_account_page:
420 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
421 return HttpResponseRedirect(self.reverse('account', node=request.node))
422 return super(AccountMultiView, self).post_register_confirm_redirect(request)
424 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
426 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
428 assert uidb36 is not None and token is not None and email is not None
431 uid_int = base36_to_int(uidb36)
435 user = get_object_or_404(User, id=uid_int)
437 email = '@'.join(email.rsplit('+', 1))
439 if email == user.email:
440 # Then short-circuit.
443 if token_generator.check_token(user, email, token):
446 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
447 if self.manage_account_page:
448 redirect = self.reverse('account', node=request.node)
450 redirect = request.node.get_absolute_url()
451 return HttpResponseRedirect(redirect)