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 current_site = Site.objects.get_current()
63 token = token_generator.make_token(user, *(token_args or []))
65 'uidb36': int_to_base36(user.id),
68 kwargs.update(reverse_kwargs or {})
69 return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
71 def display_login_page(self, request, message, extra_context=None):
72 request.session.set_test_cookie()
74 referrer = request.META.get('HTTP_REFERER', None)
76 if referrer is not None:
77 referrer = urlparse.urlparse(referrer)
79 if host != request.get_host():
82 redirect = '%s?%s' % (referrer[2], referrer[4])
85 redirect = request.node.get_absolute_url()
87 path = request.get_full_path()
90 redirect = '/'.join(path.split('/')[:-2])
91 request.session['redirect'] = redirect
94 form = LoginForm(request.POST)
97 context = self.get_context()
98 context.update(extra_context or {})
103 return self.login_page.render_to_response(request, extra_context=context)
105 def login(self, request, extra_context=None):
107 Displays the login form for the given HttpRequest.
109 if request.user.is_authenticated():
110 return HttpResponseRedirect(request.node.get_absolute_url())
112 context = self.get_context()
113 context.update(extra_context or {})
115 from django.contrib.auth.models import User
117 # If this isn't already the login page, display it.
118 if not request.POST.has_key(LOGIN_FORM_KEY):
120 message = _("Please log in again, because your session has expired.")
123 return self.display_login_page(request, message, context)
125 # Check that the user accepts cookies.
126 if not request.session.test_cookie_worked():
127 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
128 return self.display_login_page(request, message, context)
130 request.session.delete_test_cookie()
132 # Check the password.
133 username = request.POST.get('username', None)
134 password = request.POST.get('password', None)
135 user = authenticate(username=username, password=password)
137 message = ERROR_MESSAGE
138 if username is not None and u'@' in username:
139 # Mistakenly entered e-mail address instead of username? Look it up.
141 user = User.objects.get(email=username)
142 except (User.DoesNotExist, User.MultipleObjectsReturned):
143 message = _("Usernames cannot contain the '@' character.")
145 if user.check_password(password):
146 message = _("Your e-mail address is not your username."
147 " Try '%s' instead.") % user.username
149 message = _("Usernames cannot contain the '@' character.")
150 return self.display_login_page(request, message, context)
152 # The user data is correct; log in the user in and continue.
157 redirect = request.session.pop('redirect')
159 redirect = request.node.get_absolute_url()
160 return HttpResponseRedirect(redirect)
162 return self.display_login_page(request, ERROR_MESSAGE, context)
163 login = never_cache(login)
165 def logout(self, request):
166 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
168 def login_required(self, view):
169 def inner(request, *args, **kwargs):
170 if not request.user.is_authenticated():
171 return HttpResponseRedirect(self.reverse('login', node=request.node))
172 return view(request, *args, **kwargs)
176 def send_confirmation_email(self, subject, email, page, extra_context):
177 text_content = page.render_to_string(extra_context=extra_context)
178 from_email = 'noreply@%s' % Site.objects.get_current().domain
180 if page.template.mimetype == 'text/html':
181 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
182 msg.attach_alternative(text_content, 'text/html')
185 send_mail(subject, text_content, from_email, [email])
187 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
188 if request.user.is_authenticated():
189 return HttpResponseRedirect(request.node.get_absolute_url())
191 if request.method == 'POST':
192 form = PasswordResetForm(request.POST)
194 current_site = Site.objects.get_current()
195 for user in form.users_cache:
197 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
198 'username': user.username
200 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
201 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)
202 return HttpResponseRedirect('')
204 form = PasswordResetForm()
206 context = self.get_context()
207 context.update(extra_context or {})
211 return self.password_reset_page.render_to_response(request, extra_context=context)
213 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
215 Checks that a given hash in a password reset link is valid. If so,
216 displays the password set form.
218 assert uidb36 is not None and token is not None
220 uid_int = base36_to_int(uidb36)
224 user = get_object_or_404(User, id=uid_int)
226 if token_generator.check_token(user, token):
227 if request.method == 'POST':
228 form = SetPasswordForm(user, request.POST)
232 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
233 return HttpResponseRedirect(self.reverse('login', node=request.node))
235 form = SetPasswordForm(user)
237 context = self.get_context()
238 context.update(extra_context or {})
242 return self.password_set_page.render_to_response(request, extra_context=context)
246 def password_change(self, request, extra_context=None):
247 if request.method == 'POST':
248 form = PasswordChangeForm(request.user, request.POST)
251 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
252 return HttpResponseRedirect('')
254 form = PasswordChangeForm(request.user)
256 context = self.get_context()
257 context.update(extra_context or {})
261 return self.password_change_page.render_to_response(request, extra_context=context)
263 def register(self, request, extra_context=None, token_generator=registration_token_generator):
264 if request.user.is_authenticated():
265 return HttpResponseRedirect(request.node.get_absolute_url())
267 if request.method == 'POST':
268 form = RegistrationForm(request.POST)
272 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
274 current_site = Site.objects.get_current()
275 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
276 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
277 return HttpResponseRedirect(request.node.get_absolute_url())
279 form = RegistrationForm()
281 context = self.get_context()
282 context.update(extra_context or {})
286 return self.register_page.render_to_response(request, extra_context=context)
288 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
290 Checks that a given hash in a registration link is valid and activates
291 the given account. If so, log them in and redirect to
292 self.post_register_confirm_redirect.
294 assert uidb36 is not None and token is not None
296 uid_int = base36_to_int(uidb36)
300 user = get_object_or_404(User, id=uid_int)
301 if token_generator.check_token(user, token):
302 user.is_active = True
303 true_password = user.password
304 temp_password = token_generator.make_token(user)
306 user.set_password(temp_password)
308 authenticated_user = authenticate(username=user.username, password=temp_password)
309 login(request, authenticated_user)
311 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
312 user.password = true_password
314 return self.post_register_confirm_redirect(request)
318 def post_register_confirm_redirect(self, request):
319 return HttpResponseRedirect(request.node.get_absolute_url())
325 class AccountMultiView(LoginMultiView):
327 By default, the `account` consists of the first_name, last_name, and email fields
328 of the User model. Using a different account model is as simple as writing a form that
329 accepts a User instance as the first argument.
331 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
332 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
333 account_form = UserAccountForm
336 def urlpatterns(self):
337 urlpatterns = super(AccountMultiView, self).urlpatterns
338 urlpatterns += patterns('',
339 url(r'^account/$', self.login_required(self.account_view), name='account'),
340 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
344 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
345 if request.method == 'POST':
346 form = self.account_form(request.user, request.POST, request.FILES)
349 if 'email' in form.changed_data:
350 # ModelForms modify their instances in-place during validation,
351 # so reset the instance's email to its previous value here,
352 # then remove the new value from cleaned_data.
353 request.user.email = form.initial['email']
355 email = form.cleaned_data.pop('email')
358 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
360 current_site = Site.objects.get_current()
361 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
362 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
365 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
366 return HttpResponseRedirect('')
368 form = self.account_form(request.user)
370 context = self.get_context()
371 context.update(extra_context or {})
375 return self.manage_account_page.render_to_response(request, extra_context=context)
377 def has_valid_account(self, user):
378 form = self.account_form(user, {})
379 form.data = form.initial
380 return form.is_valid()
382 def account_required(self, view):
383 def inner(request, *args, **kwargs):
384 if not self.has_valid_account(request.user):
385 if not request.method == "POST":
386 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
387 return self.account_view(request, *args, **kwargs)
388 return view(request, *args, **kwargs)
390 inner = self.login_required(inner)
393 def post_register_confirm_redirect(self, request):
394 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
395 return HttpResponseRedirect(self.reverse('account', node=request.node))
397 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
399 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
401 assert uidb36 is not None and token is not None and email is not None
404 uid_int = base36_to_int(uidb36)
408 user = get_object_or_404(User, id=uid_int)
410 email = '@'.join(email.rsplit('+', 1))
412 if email == user.email:
413 # Then short-circuit.
416 if token_generator.check_token(user, email, token):
419 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
420 return HttpReponseRedirect(self.reverse('account', node=request.node))