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.core.urlresolvers import reverse
11 from django.db import models
12 from django.http import Http404, HttpResponseRedirect
13 from django.shortcuts import render_to_response, get_object_or_404
14 from django.template.defaultfilters import striptags
15 from django.utils.http import int_to_base36, base36_to_int
16 from django.utils.translation import ugettext_lazy, ugettext as _
17 from django.views.decorators.cache import never_cache
18 from django.views.decorators.csrf import csrf_protect
19 from philo.models import MultiView, Page
20 from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
21 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
25 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
28 def get_field_data(obj, fields):
30 fields = [field.name for field in obj._meta.fields if field.editable]
32 return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
35 class LoginMultiView(MultiView):
37 Handles login, registration, and forgotten passwords. In other words, this
38 multiview provides exclusively view and methods related to usernames and
41 login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
42 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
43 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
44 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
45 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
46 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
47 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
50 def urlpatterns(self):
51 urlpatterns = patterns('',
52 url(r'^login/$', self.login, name='login'),
53 url(r'^logout/$', self.logout, name='logout'),
55 url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
56 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.password_reset_confirm, name='password_reset_confirm'),
58 url(r'^register/$', csrf_protect(self.register), name='register'),
59 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.register_confirm, name='register_confirm')
62 if self.password_change_page:
63 urlpatterns += patterns('',
64 url(r'^password/change/$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
69 def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
70 current_site = Site.objects.get_current()
71 token = token_generator.make_token(user, *(token_args or []))
73 'uidb36': int_to_base36(user.id),
76 kwargs.update(reverse_kwargs or {})
77 return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
79 def get_context(self):
80 """Hook for providing instance-specific context - such as the value of a Field - to all views."""
83 def display_login_page(self, request, message, extra_context=None):
84 request.session.set_test_cookie()
86 referrer = request.META.get('HTTP_REFERER', None)
88 if referrer is not None:
89 referrer = urlparse.urlparse(referrer)
91 if host != request.get_host():
94 redirect = '%s?%s' % (referrer[2], referrer[4])
97 redirect = request.node.get_absolute_url()
99 path = request.get_full_path()
102 redirect = '/'.join(path.split('/')[:-2])
103 request.session['redirect'] = redirect
106 form = LoginForm(request.POST)
109 context = self.get_context()
110 context.update(extra_context or {})
115 return self.login_page.render_to_response(request, extra_context=context)
117 def login(self, request, extra_context=None):
119 Displays the login form for the given HttpRequest.
121 if request.user.is_authenticated():
122 return HttpResponseRedirect(request.node.get_absolute_url())
124 context = self.get_context()
125 context.update(extra_context or {})
127 from django.contrib.auth.models import User
129 # If this isn't already the login page, display it.
130 if not request.POST.has_key(LOGIN_FORM_KEY):
132 message = _("Please log in again, because your session has expired.")
135 return self.display_login_page(request, message, context)
137 # Check that the user accepts cookies.
138 if not request.session.test_cookie_worked():
139 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
140 return self.display_login_page(request, message, context)
142 request.session.delete_test_cookie()
144 # Check the password.
145 username = request.POST.get('username', None)
146 password = request.POST.get('password', None)
147 user = authenticate(username=username, password=password)
149 message = ERROR_MESSAGE
150 if username is not None and u'@' in username:
151 # Mistakenly entered e-mail address instead of username? Look it up.
153 user = User.objects.get(email=username)
154 except (User.DoesNotExist, User.MultipleObjectsReturned):
155 message = _("Usernames cannot contain the '@' character.")
157 if user.check_password(password):
158 message = _("Your e-mail address is not your username."
159 " Try '%s' instead.") % user.username
161 message = _("Usernames cannot contain the '@' character.")
162 return self.display_login_page(request, message, context)
164 # The user data is correct; log in the user in and continue.
169 redirect = request.session.pop('redirect')
171 redirect = request.node.get_absolute_url()
172 return HttpResponseRedirect(redirect)
174 return self.display_login_page(request, ERROR_MESSAGE, context)
175 login = never_cache(login)
177 def logout(self, request):
178 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
180 def login_required(self, view):
181 def inner(request, *args, **kwargs):
182 if not request.user.is_authenticated():
183 return HttpResponseRedirect(self.reverse('login', node=request.node))
184 return view(request, *args, **kwargs)
188 def send_confirmation_email(self, subject, email, page, extra_context):
189 text_content = page.render_to_string(extra_context=extra_context)
190 from_email = 'noreply@%s' % Site.objects.get_current().domain
192 if page.template.mimetype == 'text/html':
193 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
194 msg.attach_alternative(text_content, 'text/html')
197 send_mail(subject, text_content, from_email, [email])
199 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
200 if request.user.is_authenticated():
201 return HttpResponseRedirect(request.node.get_absolute_url())
203 if request.method == 'POST':
204 form = PasswordResetForm(request.POST)
206 current_site = Site.objects.get_current()
207 for user in form.users_cache:
209 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
210 'username': user.username
212 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
213 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)
214 return HttpResponseRedirect('')
216 form = PasswordResetForm()
218 context = self.get_context()
219 context.update(extra_context or {})
223 return self.password_reset_page.render_to_response(request, extra_context=context)
225 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
227 Checks that a given hash in a password reset link is valid. If so,
228 displays the password set form.
230 assert uidb36 is not None and token is not None
232 uid_int = base36_to_int(uidb36)
236 user = get_object_or_404(User, id=uid_int)
238 if token_generator.check_token(user, token):
239 if request.method == 'POST':
240 form = SetPasswordForm(user, request.POST)
244 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
245 return HttpResponseRedirect(self.reverse('login', node=request.node))
247 form = SetPasswordForm(user)
249 context = self.get_context()
250 context.update(extra_context or {})
254 return self.password_set_page.render_to_response(request, extra_context=context)
258 def password_change(self, request, extra_context=None):
259 if request.method == 'POST':
260 form = PasswordChangeForm(request.user, request.POST)
263 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
264 return HttpResponseRedirect('')
266 form = PasswordChangeForm(request.user)
268 context = self.get_context()
269 context.update(extra_context or {})
273 return self.password_change_page.render_to_response(request, extra_context=context)
275 def register(self, request, extra_context=None, token_generator=registration_token_generator):
276 if request.user.is_authenticated():
277 return HttpResponseRedirect(request.node.get_absolute_url())
279 if request.method == 'POST':
280 form = RegistrationForm(request.POST)
284 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
286 current_site = Site.objects.get_current()
287 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
288 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
289 return HttpResponseRedirect(request.node.get_absolute_url())
291 form = RegistrationForm()
293 context = self.get_context()
294 context.update(extra_context or {})
298 return self.register_page.render_to_response(request, extra_context=context)
300 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
302 Checks that a given hash in a registration link is valid and activates
303 the given account. If so, log them in and redirect to
304 self.post_register_confirm_redirect.
306 assert uidb36 is not None and token is not None
308 uid_int = base36_to_int(uidb36)
312 user = get_object_or_404(User, id=uid_int)
313 if token_generator.check_token(user, token):
314 user.is_active = True
315 true_password = user.password
316 temp_password = token_generator.make_token(user)
318 user.set_password(temp_password)
320 authenticated_user = authenticate(username=user.username, password=temp_password)
321 login(request, authenticated_user)
323 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
324 user.password = true_password
326 return self.post_register_confirm_redirect(request)
330 def post_register_confirm_redirect(self, request):
331 return HttpResponseRedirect(request.node.get_absolute_url())
337 class AccountMultiView(LoginMultiView):
339 Subclasses may define an account_profile model, fields from the User model
340 to include in the account, and fields from the account profile to use in
343 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
344 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
345 user_fields = ['first_name', 'last_name', 'email']
346 required_user_fields = user_fields
347 account_profile = None
348 account_profile_fields = None
351 def urlpatterns(self):
352 urlpatterns = super(AccountMultiView, self).urlpatterns
353 urlpatterns += patterns('',
354 url(r'^account/$', self.login_required(self.account_view), name='account'),
355 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
359 def get_account_forms(self):
360 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
362 if self.account_profile is None:
365 profile_form = forms.models.modelform_factory(self.account_profile, fields=self.account_profile_fields or [field.name for field in self.account_profile._meta.fields if field.editable and field.name != 'user'])
367 for field_name, field in user_form.base_fields.items():
368 if field_name in self.required_user_fields:
369 field.required = True
370 return user_form, profile_form
372 def get_account_form_instances(self, user, data=None):
374 user_form, profile_form = self.get_account_forms()
376 form_instances.append(user_form(instance=user))
378 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
380 form_instances.append(user_form(data, instance=user))
382 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
384 return form_instances
386 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
387 if request.method == 'POST':
388 form_instances = self.get_account_form_instances(request.user, request.POST)
389 current_email = request.user.email
391 for form in form_instances:
392 if not form.is_valid():
395 # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
396 email = request.user.email
397 if current_email != email:
399 request.user.email = current_email
401 for form in form_instances:
402 form.cleaned_data.pop('email', None)
405 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
407 current_site = Site.objects.get_current()
408 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
409 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
411 for form in form_instances:
413 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
414 return HttpResponseRedirect('')
416 form_instances = self.get_account_form_instances(request.user)
418 context = self.get_context()
419 context.update(extra_context or {})
421 'forms': form_instances
423 return self.manage_account_page.render_to_response(request, extra_context=context)
425 def has_valid_account(self, user):
426 user_form, profile_form = self.get_account_forms()
428 forms.append(user_form(data=get_field_data(user, self.user_fields)))
430 if profile_form is not None:
431 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
432 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
435 if not form.is_valid():
439 def account_required(self, view):
440 def inner(request, *args, **kwargs):
441 if not self.has_valid_account(request.user):
442 if not request.method == "POST":
443 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
444 return self.account_view(request, *args, **kwargs)
445 return view(request, *args, **kwargs)
447 inner = self.login_required(inner)
450 def post_register_confirm_redirect(self, request):
451 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
452 return HttpResponseRedirect(self.reverse('account', node=request.node))
454 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
456 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
458 assert uidb36 is not None and token is not None and email is not None
461 uid_int = base36_to_int(uidb36)
465 user = get_object_or_404(User, id=uid_int)
467 email = '@'.join(email.rsplit('+', 1))
469 if email == user.email:
470 # Then short-circuit.
473 if token_generator.check_token(user, email, token):
476 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
477 return HttpReponseRedirect(self.reverse('account', node=request.node))