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 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.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
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 def get_field_data(obj, fields):
29 fields = [field.name for field in obj._meta.fields if field.editable]
31 return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
34 class LoginMultiView(MultiView):
36 Handles login, registration, and forgotten passwords. In other words, this
37 multiview provides exclusively view and methods related to usernames and
40 login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
41 password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
42 password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
43 password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
44 password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
45 register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
46 register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
49 def urlpatterns(self):
50 urlpatterns = patterns('',
51 url(r'^login/$', self.login, name='login'),
52 url(r'^logout/$', self.logout, name='logout'),
54 url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
55 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.password_reset_confirm, name='password_reset_confirm'),
57 url(r'^register/$', csrf_protect(self.register), name='register'),
58 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.register_confirm, name='register_confirm')
61 if self.password_change_page:
62 urlpatterns += patterns('',
63 url(r'^password/change/$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
68 def get_context(self, extra_dict=None):
70 context.update(extra_dict or {})
73 def display_login_page(self, request, message, node=None, extra_context=None):
74 request.session.set_test_cookie()
76 referrer = request.META.get('HTTP_REFERER', None)
78 if referrer is not None:
79 referrer = urlparse.urlparse(referrer)
81 if host != request.get_host():
84 redirect = '%s?%s' % (referrer[2], referrer[4])
87 redirect = node.get_absolute_url()
89 path = request.get_full_path()
92 redirect = '/'.join(path.split('/')[:-2])
93 request.session['redirect'] = redirect
96 form = LoginForm(request.POST)
99 context = self.get_context({
103 context.update(extra_context or {})
104 return self.login_page.render_to_response(node, request, extra_context=context)
106 def login(self, request, node=None, extra_context=None):
108 Displays the login form for the given HttpRequest.
110 if request.user.is_authenticated():
111 return HttpResponseRedirect(node.get_absolute_url())
113 context = self.get_context(extra_context)
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, node, 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, node, 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, node, context)
152 # The user data is correct; log in the user in and continue.
157 redirect = request.session.pop('redirect')
159 redirect = node.get_absolute_url()
160 return HttpResponseRedirect(redirect)
162 return self.display_login_page(request, ERROR_MESSAGE, node, context)
163 login = never_cache(login)
165 def logout(self, request):
166 return auth_views.logout(request, request.META['HTTP_REFERER'])
168 def login_required(self, view):
169 def inner(request, node=None, *args, **kwargs):
170 if not request.user.is_authenticated():
171 login_url = reverse('login', urlconf=self).strip('/')
172 return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
173 return view(request, node=node, *args, **kwargs)
177 def send_confirmation_email(self, subject, email, page, extra_context):
178 message = page.render_to_string(extra_context=extra_context)
179 from_email = 'noreply@%s' % Site.objects.get_current().domain
180 send_mail(subject, message, from_email, [email])
182 def password_reset(self, request, node=None, extra_context=None, token_generator=password_token_generator):
183 if request.user.is_authenticated():
184 return HttpResponseRedirect(node.get_absolute_url())
186 if request.method == 'POST':
187 form = PasswordResetForm(request.POST)
189 current_site = Site.objects.get_current()
190 for user in form.users_cache:
191 token = token_generator.make_token(user)
192 link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
195 'username': user.username
197 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
198 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)
199 return HttpResponseRedirect('')
201 form = PasswordResetForm()
203 context = self.get_context({'form': form})
204 context.update(extra_context or {})
205 return self.password_reset_page.render_to_response(node, request, extra_context=context)
207 def password_reset_confirm(self, request, node=None, 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 = SetPasswordForm(user, request.POST)
226 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
227 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
229 form = SetPasswordForm(user)
231 context = self.get_context({'form': form})
232 return self.password_set_page.render_to_response(node, request, extra_context=context)
236 def password_change(self, request, node=None, extra_context=None):
237 if request.method == 'POST':
238 form = PasswordChangeForm(request.user, request.POST)
241 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
242 return HttpResponseRedirect('')
244 form = PasswordChangeForm(request.user)
246 context = self.get_context({'form': form})
247 context.update(extra_context or {})
248 return self.password_change_page.render_to_response(node, request, extra_context=context)
250 def register(self, request, node=None, extra_context=None, token_generator=registration_token_generator):
251 if request.user.is_authenticated():
252 return HttpResponseRedirect(node.get_absolute_url())
254 if request.method == 'POST':
255 form = RegistrationForm(request.POST)
258 current_site = Site.objects.get_current()
259 token = token_generator.make_token(user)
260 link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
264 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
265 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
266 return HttpResponseRedirect(node.get_absolute_url())
268 form = RegistrationForm()
270 context = self.get_context({'form': form})
271 context.update(extra_context or {})
272 return self.register_page.render_to_response(node, request, extra_context=context)
274 def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
276 Checks that a given hash in a registration link is valid and activates
277 the given account. If so, log them in and redirect to
278 self.post_register_confirm_redirect.
280 assert uidb36 is not None and token is not None
282 uid_int = base36_to_int(uidb36)
286 user = get_object_or_404(User, id=uid_int)
287 if token_generator.check_token(user, token):
288 user.is_active = True
289 true_password = user.password
291 user.set_password('temp_password')
293 authenticated_user = authenticate(username=user.username, password='temp_password')
294 login(request, authenticated_user)
296 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
297 user.password = true_password
299 return self.post_register_confirm_redirect(request, node)
303 def post_register_confirm_redirect(self, request, node):
304 return HttpResponseRedirect(node.get_absolute_url())
310 class AccountMultiView(LoginMultiView):
312 Subclasses may define an account_profile model, fields from the User model
313 to include in the account, and fields from the account profile to use in
316 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
317 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
318 user_fields = ['first_name', 'last_name', 'email']
319 required_user_fields = user_fields
320 account_profile = None
321 account_profile_fields = None
324 def urlpatterns(self):
325 urlpatterns = super(AccountMultiView, self).urlpatterns
326 urlpatterns += patterns('',
327 url(r'^account/$', self.login_required(self.account_view), name='account'),
328 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
332 def get_account_forms(self):
333 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
335 if self.account_profile is None:
338 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'])
340 for field_name, field in user_form.base_fields.items():
341 if field_name in self.required_user_fields:
342 field.required = True
343 return user_form, profile_form
345 def get_account_form_instances(self, user, data=None):
347 user_form, profile_form = self.get_account_forms()
349 form_instances.append(user_form(instance=user))
351 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
353 form_instances.append(user_form(data, instance=user))
355 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
357 return form_instances
359 def account_view(self, request, node=None, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
360 if request.method == 'POST':
361 form_instances = self.get_account_form_instances(request.user, request.POST)
362 current_email = request.user.email
364 for form in form_instances:
365 if not form.is_valid():
368 # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
369 email = request.user.email
370 if current_email != email:
372 request.user.email = current_email
374 for form in form_instances:
375 form.cleaned_data.pop('email', None)
377 current_site = Site.objects.get_current()
378 token = token_generator.make_token(request.user, email)
379 link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('email_change_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(request.user.id), 'email': email.replace('@', '+'), 'token': token}).strip('/'))
383 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
384 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
386 for form in form_instances:
388 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
389 return HttpResponseRedirect('')
391 form_instances = self.get_account_form_instances(request.user)
393 context = self.get_context({
394 'forms': form_instances
396 context.update(extra_context or {})
397 return self.manage_account_page.render_to_response(node, request, extra_context=context)
399 def has_valid_account(self, user):
400 user_form, profile_form = self.get_account_forms()
402 forms.append(user_form(data=get_field_data(user, self.user_fields)))
404 if profile_form is not None:
405 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
406 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
409 if not form.is_valid():
413 def account_required(self, view):
414 def inner(request, *args, **kwargs):
415 if not self.has_valid_account(request.user):
416 if not request.method == "POST":
417 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
418 return self.account_view(request, *args, **kwargs)
419 return view(request, *args, **kwargs)
421 inner = self.login_required(inner)
424 def post_register_confirm_redirect(self, request, node):
425 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
426 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
428 def email_change_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
430 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
432 assert uidb36 is not None and token is not None and email is not None
435 uid_int = base36_to_int(uidb36)
439 user = get_object_or_404(User, id=uid_int)
441 email = '@'.join(email.rsplit('+', 1))
443 if email == user.email:
444 # Then short-circuit.
447 if token_generator.check_token(user, email, token):
450 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
451 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))