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 get_context(self):
72 """Hook for providing instance-specific context - such as the value of a Field - to all views."""
75 def display_login_page(self, request, message, extra_context=None):
76 request.session.set_test_cookie()
78 referrer = request.META.get('HTTP_REFERER', None)
80 if referrer is not None:
81 referrer = urlparse.urlparse(referrer)
83 if host != request.get_host():
86 redirect = '%s?%s' % (referrer[2], referrer[4])
89 redirect = request.node.get_absolute_url()
91 path = request.get_full_path()
94 redirect = '/'.join(path.split('/')[:-2])
95 request.session['redirect'] = redirect
98 form = LoginForm(request.POST)
101 context = self.get_context()
102 context.update(extra_context or {})
107 return self.login_page.render_to_response(request, extra_context=context)
109 def login(self, request, extra_context=None):
111 Displays the login form for the given HttpRequest.
113 if request.user.is_authenticated():
114 return HttpResponseRedirect(request.node.get_absolute_url())
116 context = self.get_context()
117 context.update(extra_context or {})
119 from django.contrib.auth.models import User
121 # If this isn't already the login page, display it.
122 if not request.POST.has_key(LOGIN_FORM_KEY):
124 message = _("Please log in again, because your session has expired.")
127 return self.display_login_page(request, message, context)
129 # Check that the user accepts cookies.
130 if not request.session.test_cookie_worked():
131 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
132 return self.display_login_page(request, message, context)
134 request.session.delete_test_cookie()
136 # Check the password.
137 username = request.POST.get('username', None)
138 password = request.POST.get('password', None)
139 user = authenticate(username=username, password=password)
141 message = ERROR_MESSAGE
142 if username is not None and u'@' in username:
143 # Mistakenly entered e-mail address instead of username? Look it up.
145 user = User.objects.get(email=username)
146 except (User.DoesNotExist, User.MultipleObjectsReturned):
147 message = _("Usernames cannot contain the '@' character.")
149 if user.check_password(password):
150 message = _("Your e-mail address is not your username."
151 " Try '%s' instead.") % user.username
153 message = _("Usernames cannot contain the '@' character.")
154 return self.display_login_page(request, message, context)
156 # The user data is correct; log in the user in and continue.
161 redirect = request.session.pop('redirect')
163 redirect = request.node.get_absolute_url()
164 return HttpResponseRedirect(redirect)
166 return self.display_login_page(request, ERROR_MESSAGE, context)
167 login = never_cache(login)
169 def logout(self, request):
170 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
172 def login_required(self, view):
173 def inner(request, *args, **kwargs):
174 if not request.user.is_authenticated():
175 return HttpResponseRedirect(self.reverse('login', node=request.node))
176 return view(request, *args, **kwargs)
180 def send_confirmation_email(self, subject, email, page, extra_context):
181 text_content = page.render_to_string(extra_context=extra_context)
182 from_email = 'noreply@%s' % Site.objects.get_current().domain
184 if page.template.mimetype == 'text/html':
185 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
186 msg.attach_alternative(text_content, 'text/html')
189 send_mail(subject, text_content, from_email, [email])
191 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
192 if request.user.is_authenticated():
193 return HttpResponseRedirect(request.node.get_absolute_url())
195 if request.method == 'POST':
196 form = PasswordResetForm(request.POST)
198 current_site = Site.objects.get_current()
199 for user in form.users_cache:
201 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
202 'username': user.username
204 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
205 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)
206 return HttpResponseRedirect('')
208 form = PasswordResetForm()
210 context = self.get_context()
211 context.update(extra_context or {})
215 return self.password_reset_page.render_to_response(request, extra_context=context)
217 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
219 Checks that a given hash in a password reset link is valid. If so,
220 displays the password set form.
222 assert uidb36 is not None and token is not None
224 uid_int = base36_to_int(uidb36)
228 user = get_object_or_404(User, id=uid_int)
230 if token_generator.check_token(user, token):
231 if request.method == 'POST':
232 form = SetPasswordForm(user, request.POST)
236 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
237 return HttpResponseRedirect(self.reverse('login', node=request.node))
239 form = SetPasswordForm(user)
241 context = self.get_context()
242 context.update(extra_context or {})
246 return self.password_set_page.render_to_response(request, extra_context=context)
250 def password_change(self, request, extra_context=None):
251 if request.method == 'POST':
252 form = PasswordChangeForm(request.user, request.POST)
255 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
256 return HttpResponseRedirect('')
258 form = PasswordChangeForm(request.user)
260 context = self.get_context()
261 context.update(extra_context or {})
265 return self.password_change_page.render_to_response(request, extra_context=context)
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, ABSOLUTELY 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(LoginMultiView):
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')
336 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
337 account_form = UserAccountForm
340 def urlpatterns(self):
341 urlpatterns = super(AccountMultiView, self).urlpatterns
342 urlpatterns += patterns('',
343 url(r'^account/$', self.login_required(self.account_view), name='account'),
344 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
348 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
349 if request.method == 'POST':
350 form = self.account_form(request.user, request.POST, request.FILES)
353 if 'email' in form.changed_data:
354 # ModelForms modify their instances in-place during validation,
355 # so reset the instance's email to its previous value here,
356 # then remove the new value from cleaned_data.
357 request.user.email = form.initial['email']
359 email = form.cleaned_data.pop('email')
362 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
364 current_site = Site.objects.get_current()
365 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
366 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
369 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
370 return HttpResponseRedirect('')
372 form = self.account_form(request.user)
374 context = self.get_context()
375 context.update(extra_context or {})
379 return self.manage_account_page.render_to_response(request, extra_context=context)
381 def has_valid_account(self, user):
382 user_form, profile_form = self.get_account_forms()
384 forms.append(user_form(data=get_field_data(user, self.user_fields)))
386 if profile_form is not None:
387 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
388 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
391 if not form.is_valid():
395 def account_required(self, view):
396 def inner(request, *args, **kwargs):
397 if not self.has_valid_account(request.user):
398 if not request.method == "POST":
399 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
400 return self.account_view(request, *args, **kwargs)
401 return view(request, *args, **kwargs)
403 inner = self.login_required(inner)
406 def post_register_confirm_redirect(self, request):
407 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
408 return HttpResponseRedirect(self.reverse('account', node=request.node))
410 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
412 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
414 assert uidb36 is not None and token is not None and email is not None
417 uid_int = base36_to_int(uidb36)
421 user = get_object_or_404(User, id=uid_int)
423 email = '@'.join(email.rsplit('+', 1))
425 if email == user.email:
426 # Then short-circuit.
429 if token_generator.check_token(user, email, token):
432 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
433 return HttpReponseRedirect(self.reverse('account', node=request.node))