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_lazy, 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 LOGIN_FORM_KEY, LoginForm, RegistrationForm, UserAccountForm
20 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
24 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
27 class LoginMultiView(MultiView):
29 Handles login, registration, and forgotten passwords. In other words, this
30 multiview provides exclusively view and methods related to usernames and
33 login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
34 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
35 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
36 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
37 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
38 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
39 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
42 def urlpatterns(self):
43 urlpatterns = patterns('',
44 url(r'^login$', self.login, name='login'),
45 url(r'^logout$', self.logout, name='logout'),
47 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
48 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
50 url(r'^register$', csrf_protect(self.register), name='register'),
51 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
54 if self.password_change_page:
55 urlpatterns += patterns('',
56 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
61 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
62 token = token_generator.make_token(user, *(token_args or []))
64 'uidb36': int_to_base36(user.id),
67 kwargs.update(reverse_kwargs or {})
68 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
70 def display_login_page(self, request, message, extra_context=None):
71 request.session.set_test_cookie()
73 referrer = request.META.get('HTTP_REFERER', None)
75 if referrer is not None:
76 referrer = urlparse.urlparse(referrer)
78 if host != request.get_host():
81 redirect = '%s?%s' % (referrer[2], referrer[4])
84 redirect = request.node.get_absolute_url()
86 path = request.get_full_path()
89 redirect = '/'.join(path.split('/')[:-2])
90 request.session['redirect'] = redirect
93 form = LoginForm(request.POST)
96 context = self.get_context()
97 context.update(extra_context or {})
102 return self.login_page.render_to_response(request, extra_context=context)
104 def login(self, request, extra_context=None):
106 Displays the login form for the given HttpRequest.
108 if request.user.is_authenticated():
109 return HttpResponseRedirect(request.node.get_absolute_url())
111 context = self.get_context()
112 context.update(extra_context or {})
114 from django.contrib.auth.models import User
116 # If this isn't already the login page, display it.
117 if not request.POST.has_key(LOGIN_FORM_KEY):
119 message = _("Please log in again, because your session has expired.")
122 return self.display_login_page(request, message, context)
124 # Check that the user accepts cookies.
125 if not request.session.test_cookie_worked():
126 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
127 return self.display_login_page(request, message, context)
129 request.session.delete_test_cookie()
131 # Check the password.
132 username = request.POST.get('username', None)
133 password = request.POST.get('password', None)
134 user = authenticate(username=username, password=password)
136 message = ERROR_MESSAGE
137 if username is not None and u'@' in username:
138 # Mistakenly entered e-mail address instead of username? Look it up.
140 user = User.objects.get(email=username)
141 except (User.DoesNotExist, User.MultipleObjectsReturned):
142 message = _("Usernames cannot contain the '@' character.")
144 if user.check_password(password):
145 message = _("Your e-mail address is not your username."
146 " Try '%s' instead.") % user.username
148 message = _("Usernames cannot contain the '@' character.")
149 return self.display_login_page(request, message, context)
151 # The user data is correct; log in the user in and continue.
156 redirect = request.session.pop('redirect')
158 redirect = request.node.get_absolute_url()
159 return HttpResponseRedirect(redirect)
161 return self.display_login_page(request, ERROR_MESSAGE, context)
162 login = never_cache(login)
164 def logout(self, request):
165 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
167 def login_required(self, view):
168 def inner(request, *args, **kwargs):
169 if not request.user.is_authenticated():
170 return HttpResponseRedirect(self.reverse('login', node=request.node))
171 return view(request, *args, **kwargs)
175 def send_confirmation_email(self, subject, email, page, extra_context):
176 text_content = page.render_to_string(extra_context=extra_context)
177 from_email = 'noreply@%s' % Site.objects.get_current().domain
179 if page.template.mimetype == 'text/html':
180 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
181 msg.attach_alternative(text_content, 'text/html')
184 send_mail(subject, text_content, from_email, [email])
186 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
187 if request.user.is_authenticated():
188 return HttpResponseRedirect(request.node.get_absolute_url())
190 if request.method == 'POST':
191 form = PasswordResetForm(request.POST)
193 current_site = Site.objects.get_current()
194 for user in form.users_cache:
196 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
197 'username': user.username
199 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
200 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)
201 return HttpResponseRedirect('')
203 form = PasswordResetForm()
205 context = self.get_context()
206 context.update(extra_context or {})
210 return self.password_reset_page.render_to_response(request, extra_context=context)
212 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
214 Checks that a given hash in a password reset link is valid. If so,
215 displays the password set form.
217 assert uidb36 is not None and token is not None
219 uid_int = base36_to_int(uidb36)
223 user = get_object_or_404(User, id=uid_int)
225 if token_generator.check_token(user, token):
226 if request.method == 'POST':
227 form = SetPasswordForm(user, request.POST)
231 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
232 return HttpResponseRedirect(self.reverse('login', node=request.node))
234 form = SetPasswordForm(user)
236 context = self.get_context()
237 context.update(extra_context or {})
241 return self.password_set_page.render_to_response(request, extra_context=context)
245 def password_change(self, request, extra_context=None):
246 if request.method == 'POST':
247 form = PasswordChangeForm(request.user, request.POST)
250 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
251 return HttpResponseRedirect('')
253 form = PasswordChangeForm(request.user)
255 context = self.get_context()
256 context.update(extra_context or {})
260 return self.password_change_page.render_to_response(request, extra_context=context)
262 def register(self, request, extra_context=None, token_generator=registration_token_generator):
263 if request.user.is_authenticated():
264 return HttpResponseRedirect(request.node.get_absolute_url())
266 if request.method == 'POST':
267 form = RegistrationForm(request.POST)
271 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
273 current_site = Site.objects.get_current()
274 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
275 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
276 return HttpResponseRedirect(request.node.get_absolute_url())
278 form = RegistrationForm()
280 context = self.get_context()
281 context.update(extra_context or {})
285 return self.register_page.render_to_response(request, extra_context=context)
287 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
289 Checks that a given hash in a registration link is valid and activates
290 the given account. If so, log them in and redirect to
291 self.post_register_confirm_redirect.
293 assert uidb36 is not None and token is not None
295 uid_int = base36_to_int(uidb36)
299 user = get_object_or_404(User, id=uid_int)
300 if token_generator.check_token(user, token):
301 user.is_active = True
302 true_password = user.password
303 temp_password = token_generator.make_token(user)
305 user.set_password(temp_password)
307 authenticated_user = authenticate(username=user.username, password=temp_password)
308 login(request, authenticated_user)
310 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
311 user.password = true_password
313 return self.post_register_confirm_redirect(request)
317 def post_register_confirm_redirect(self, request):
318 return HttpResponseRedirect(request.node.get_absolute_url())
324 class AccountMultiView(LoginMultiView):
326 By default, the `account` consists of the first_name, last_name, and email fields
327 of the User model. Using a different account model is as simple as writing a form that
328 accepts a User instance as the first argument.
330 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
331 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
332 account_form = UserAccountForm
335 def urlpatterns(self):
336 urlpatterns = super(AccountMultiView, self).urlpatterns
337 urlpatterns += patterns('',
338 url(r'^account$', self.login_required(self.account_view), name='account'),
339 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
343 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
344 if request.method == 'POST':
345 form = self.account_form(request.user, request.POST, request.FILES)
348 if 'email' in form.changed_data:
349 # ModelForms modify their instances in-place during validation,
350 # so reset the instance's email to its previous value here,
351 # then remove the new value from cleaned_data.
352 request.user.email = form.initial['email']
354 email = form.cleaned_data.pop('email')
357 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
359 current_site = Site.objects.get_current()
360 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
361 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
364 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
365 return HttpResponseRedirect('')
367 form = self.account_form(request.user)
369 context = self.get_context()
370 context.update(extra_context or {})
374 return self.manage_account_page.render_to_response(request, extra_context=context)
376 def has_valid_account(self, user):
377 form = self.account_form(user, {})
378 form.data = form.initial
379 return form.is_valid()
381 def account_required(self, view):
382 def inner(request, *args, **kwargs):
383 if not self.has_valid_account(request.user):
384 if not request.method == "POST":
385 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
386 return self.account_view(request, *args, **kwargs)
387 return view(request, *args, **kwargs)
389 inner = self.login_required(inner)
392 def post_register_confirm_redirect(self, request):
393 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
394 return HttpResponseRedirect(self.reverse('account', node=request.node))
396 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
398 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
400 assert uidb36 is not None and token is not None and email is not None
403 uid_int = base36_to_int(uidb36)
407 user = get_object_or_404(User, id=uid_int)
409 email = '@'.join(email.rsplit('+', 1))
411 if email == user.email:
412 # Then short-circuit.
415 if token_generator.check_token(user, email, token):
418 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
419 return HttpResponseRedirect(self.reverse('account', node=request.node))