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)
136 password_change_form = PasswordChangeForm
137 password_set_form = SetPasswordForm
138 password_reset_form = PasswordResetForm
141 def urlpatterns(self):
142 urlpatterns = super(PasswordMultiView, self).urlpatterns
144 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
145 urlpatterns += patterns('',
146 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
147 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
150 if self.password_change_page:
151 urlpatterns += patterns('',
152 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
156 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
157 token = token_generator.make_token(user, *(token_args or []))
159 'uidb36': int_to_base36(user.id),
162 kwargs.update(reverse_kwargs or {})
163 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
165 def send_confirmation_email(self, subject, email, page, extra_context):
166 text_content = page.render_to_string(extra_context=extra_context)
167 from_email = 'noreply@%s' % Site.objects.get_current().domain
169 if page.template.mimetype == 'text/html':
170 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
171 msg.attach_alternative(text_content, 'text/html')
174 send_mail(subject, text_content, from_email, [email])
176 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
177 if request.user.is_authenticated():
178 return HttpResponseRedirect(request.node.get_absolute_url())
180 if request.method == 'POST':
181 form = self.password_reset_form(request.POST)
183 current_site = Site.objects.get_current()
184 for user in form.users_cache:
186 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
188 'site': current_site,
191 # Deprecated... leave in for backwards-compatibility
192 'username': user.username
194 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
195 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)
196 return HttpResponseRedirect('')
198 form = self.password_reset_form()
200 context = self.get_context()
201 context.update(extra_context or {})
205 return self.password_reset_page.render_to_response(request, extra_context=context)
207 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
209 Checks that a given hash in a password reset link is valid. If so,
210 displays the password set form.
212 assert uidb36 is not None and token is not None
214 uid_int = base36_to_int(uidb36)
218 user = get_object_or_404(User, id=uid_int)
220 if token_generator.check_token(user, token):
221 if request.method == 'POST':
222 form = self.password_set_form(user, request.POST)
226 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
227 return HttpResponseRedirect(self.reverse('login', node=request.node))
229 form = self.password_set_form(user)
231 context = self.get_context()
232 context.update(extra_context or {})
236 return self.password_set_page.render_to_response(request, extra_context=context)
240 def password_change(self, request, extra_context=None):
241 if request.method == 'POST':
242 form = self.password_change_form(request.user, request.POST)
245 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
246 return HttpResponseRedirect('')
248 form = self.password_change_form(request.user)
250 context = self.get_context()
251 context.update(extra_context or {})
255 return self.password_change_page.render_to_response(request, extra_context=context)
261 class RegistrationMultiView(PasswordMultiView):
262 """Adds on the pages necessary for letting new users register."""
263 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
264 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
265 registration_form = RegistrationForm
268 def urlpatterns(self):
269 urlpatterns = super(RegistrationMultiView, self).urlpatterns
270 if self.register_page and self.register_confirmation_email:
271 urlpatterns += patterns('',
272 url(r'^register$', csrf_protect(self.register), name='register'),
273 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
277 def register(self, request, extra_context=None, token_generator=registration_token_generator):
278 if request.user.is_authenticated():
279 return HttpResponseRedirect(request.node.get_absolute_url())
281 if request.method == 'POST':
282 form = self.registration_form(request.POST)
285 current_site = Site.objects.get_current()
287 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node, secure=request.is_secure()),
289 'site': current_site,
292 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
293 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
294 return HttpResponseRedirect(request.node.get_absolute_url())
296 form = self.registration_form()
298 context = self.get_context()
299 context.update(extra_context or {})
303 return self.register_page.render_to_response(request, extra_context=context)
305 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
307 Checks that a given hash in a registration link is valid and activates
308 the given account. If so, log them in and redirect to
309 self.post_register_confirm_redirect.
311 assert uidb36 is not None and token is not None
313 uid_int = base36_to_int(uidb36)
317 user = get_object_or_404(User, id=uid_int)
318 if token_generator.check_token(user, token):
319 user.is_active = True
320 true_password = user.password
321 temp_password = token_generator.make_token(user)
323 user.set_password(temp_password)
325 authenticated_user = authenticate(username=user.username, password=temp_password)
326 login(request, authenticated_user)
328 # if anything goes wrong, do our best make sure that the true password is restored.
329 user.password = true_password
331 return self.post_register_confirm_redirect(request)
335 def post_register_confirm_redirect(self, request):
336 return HttpResponseRedirect(request.node.get_absolute_url())
342 class AccountMultiView(RegistrationMultiView):
344 By default, the `account` consists of the first_name, last_name, and email fields
345 of the User model. Using a different account model is as simple as writing a form that
346 accepts a User instance as the first argument.
348 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
349 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.")
351 account_form = UserAccountForm
354 def urlpatterns(self):
355 urlpatterns = super(AccountMultiView, self).urlpatterns
356 if self.manage_account_page:
357 urlpatterns += patterns('',
358 url(r'^account$', self.login_required(self.account_view), name='account'),
360 if self.email_change_confirmation_email:
361 urlpatterns += patterns('',
362 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
366 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
367 if request.method == 'POST':
368 form = self.account_form(request.user, request.POST, request.FILES)
371 message = "Account information saved."
372 redirect = self.get_requirement_redirect(request, default='')
373 if 'email' in form.changed_data and self.email_change_confirmation_email:
374 # ModelForms modify their instances in-place during
375 # validation, so reset the instance's email to its
376 # previous value here, then remove the new value
377 # from cleaned_data. We only do this if an email
378 # change confirmation email is available.
379 request.user.email = form.initial['email']
381 email = form.cleaned_data.pop('email')
383 current_site = Site.objects.get_current()
386 '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()),
387 'user': request.user,
388 'site': current_site,
391 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
393 message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
394 if not request.user.email:
395 message += " You will need to confirm the email before accessing pages that require a valid account."
401 message += " Here you go!"
403 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
404 return HttpResponseRedirect(redirect)
406 form = self.account_form(request.user)
408 context = self.get_context()
409 context.update(extra_context or {})
413 return self.manage_account_page.render_to_response(request, extra_context=context)
415 def has_valid_account(self, user):
416 form = self.account_form(user, {})
417 form.data = form.initial
418 return form.is_valid()
420 def account_required(self, view):
421 def inner(request, *args, **kwargs):
422 if not self.has_valid_account(request.user):
423 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
424 if self.manage_account_page:
425 self.set_requirement_redirect(request, redirect=request.path)
426 redirect = self.reverse('account', node=request.node)
428 redirect = node.get_absolute_url()
429 return HttpResponseRedirect(redirect)
430 return view(request, *args, **kwargs)
432 inner = self.login_required(inner)
435 def post_register_confirm_redirect(self, request):
436 if self.manage_account_page:
437 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
438 return HttpResponseRedirect(self.reverse('account', node=request.node))
439 return super(AccountMultiView, self).post_register_confirm_redirect(request)
441 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
443 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
445 assert uidb36 is not None and token is not None and email is not None
448 uid_int = base36_to_int(uidb36)
452 user = get_object_or_404(User, id=uid_int)
454 email = '@'.join(email.rsplit('+', 1))
456 if email == user.email:
457 # Then short-circuit.
460 if token_generator.check_token(user, email, token):
463 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
464 if self.manage_account_page:
465 redirect = self.reverse('account', node=request.node)
467 redirect = request.node.get_absolute_url()
468 return HttpResponseRedirect(redirect)