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 = ''.join(referrer[2:])
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 context = self.get_context(extra_context)
112 from django.contrib.auth.models import User
114 # If this isn't already the login page, display it.
115 if not request.POST.has_key(LOGIN_FORM_KEY):
117 message = _("Please log in again, because your session has expired.")
120 return self.display_login_page(request, message, node, context)
122 # Check that the user accepts cookies.
123 if not request.session.test_cookie_worked():
124 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
125 return self.display_login_page(request, message, node, context)
127 request.session.delete_test_cookie()
129 # Check the password.
130 username = request.POST.get('username', None)
131 password = request.POST.get('password', None)
132 user = authenticate(username=username, password=password)
134 message = ERROR_MESSAGE
135 if username is not None and u'@' in username:
136 # Mistakenly entered e-mail address instead of username? Look it up.
138 user = User.objects.get(email=username)
139 except (User.DoesNotExist, User.MultipleObjectsReturned):
140 message = _("Usernames cannot contain the '@' character.")
142 if user.check_password(password):
143 message = _("Your e-mail address is not your username."
144 " Try '%s' instead.") % user.username
146 message = _("Usernames cannot contain the '@' character.")
147 return self.display_login_page(request, message, node, context)
149 # The user data is correct; log in the user in and continue.
154 redirect = request.session.pop('redirect')
156 redirect = node.get_absolute_url()
157 return HttpResponseRedirect(redirect)
159 return self.display_login_page(request, ERROR_MESSAGE, node, context)
160 login = never_cache(login)
162 def logout(self, request):
163 return auth_views.logout(request, request.META['HTTP_REFERER'])
165 def login_required(self, view):
166 def inner(request, node=None, *args, **kwargs):
167 if not request.user.is_authenticated():
168 login_url = reverse('login', urlconf=self).strip('/')
169 return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
170 return view(request, node=node, *args, **kwargs)
174 def send_confirmation_email(self, subject, email, page, extra_context):
175 message = page.render_to_string(extra_context=extra_context)
176 from_email = 'noreply@%s' % Site.objects.get_current().domain
177 send_mail(subject, message, from_email, [email])
179 def password_reset(self, request, node=None, extra_context=None, token_generator=password_token_generator):
180 if request.method == 'POST':
181 form = PasswordResetForm(request.POST)
183 current_site = Site.objects.get_current()
184 for user in form.users_cache:
185 token = token_generator.make_token(user)
186 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('/'))
189 'username': user.username
191 self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
192 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)
193 return HttpResponseRedirect('')
195 form = PasswordResetForm()
197 context = self.get_context({'form': form})
198 context.update(extra_context or {})
199 return self.password_reset_page.render_to_response(node, request, extra_context=context)
201 def password_reset_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
203 Checks that a given hash in a password reset link is valid. If so,
204 displays the password set form.
206 assert uidb36 is not None and token is not None
208 uid_int = base36_to_int(uidb36)
212 user = get_object_or_404(User, id=uid_int)
214 if token_generator.check_token(user, token):
215 if request.method == 'POST':
216 form = SetPasswordForm(user, request.POST)
220 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
221 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
223 form = SetPasswordForm(user)
225 context = self.get_context({'form': form})
226 return self.password_set_page.render_to_response(node, request, extra_context=context)
230 def password_change(self, request, node=None, extra_context=None):
231 if request.method == 'POST':
232 form = PasswordChangeForm(request.user, request.POST)
235 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
236 return HttpResponseRedirect('')
238 form = PasswordChangeForm(request.user)
240 context = self.get_context({'form': form})
241 context.update(extra_context or {})
242 return self.password_change_page.render_to_response(node, request, extra_context=context)
244 def register(self, request, node=None, extra_context=None, token_generator=registration_token_generator):
245 if request.user.is_authenticated():
246 return HttpResponseRedirect(node.get_absolute_url())
248 if request.method == 'POST':
249 form = RegistrationForm(request.POST)
252 current_site = Site.objects.get_current()
253 token = token_generator.make_token(user)
254 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('/'))
258 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
259 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
260 return HttpResponseRedirect('')
262 form = RegistrationForm()
264 context = self.get_context({'form': form})
265 context.update(extra_context or {})
266 return self.register_page.render_to_response(node, request, extra_context=context)
268 def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
270 Checks that a given hash in a registration link is valid and activates
271 the given account. If so, log them in and redirect to
272 self.post_register_confirm_redirect.
274 assert uidb36 is not None and token is not None
276 uid_int = base36_to_int(uidb36)
280 user = get_object_or_404(User, id=uid_int)
281 if token_generator.check_token(user, token):
282 user.is_active = True
283 true_password = user.password
285 user.set_password('temp_password')
287 authenticated_user = authenticate(username=user.username, password='temp_password')
288 login(request, authenticated_user)
290 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
291 user.password = true_password
293 return self.post_register_confirm_redirect(request, node)
297 def post_register_confirm_redirect(self, request, node):
298 return HttpResponseRedirect(node.get_absolute_url())
304 class AccountMultiView(LoginMultiView):
306 Subclasses may define an account_profile model, fields from the User model
307 to include in the account, and fields from the account profile to use in
310 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
311 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
312 user_fields = ['first_name', 'last_name', 'email']
313 required_user_fields = user_fields
314 account_profile = None
315 account_profile_fields = None
318 def urlpatterns(self):
319 urlpatterns = super(AccountMultiView, self).urlpatterns
320 urlpatterns += patterns('',
321 url(r'^account/$', self.login_required(self.account_view), name='account'),
322 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
326 def get_account_forms(self):
327 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
329 if self.account_profile is None:
332 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'])
334 for field_name, field in user_form.base_fields.items():
335 if field_name in self.required_user_fields:
336 field.required = True
337 return user_form, profile_form
339 def get_account_form_instances(self, user, data=None):
341 user_form, profile_form = self.get_account_forms()
343 form_instances.append(user_form(instance=user))
345 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
347 form_instances.append(user_form(data, instance=user))
349 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
351 return form_instances
353 def account_view(self, request, node=None, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
354 if request.method == 'POST':
355 form_instances = self.get_account_form_instances(request.user, request.POST)
356 current_email = request.user.email
358 for form in form_instances:
359 if not form.is_valid():
362 # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
363 email = request.user.email
364 if current_email != email:
366 request.user.email = current_email
368 for form in form_instances:
369 form.cleaned_data.pop('email', None)
371 current_site = Site.objects.get_current()
372 token = token_generator.make_token(request.user, email)
373 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('/'))
377 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
378 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
380 for form in form_instances:
382 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
383 return HttpResponseRedirect('')
385 form_instances = self.get_account_form_instances(request.user)
387 context = self.get_context({
388 'forms': form_instances
390 context.update(extra_context or {})
391 return self.manage_account_page.render_to_response(node, request, extra_context=context)
393 def has_valid_account(self, user):
394 user_form, profile_form = self.get_account_forms()
396 forms.append(user_form(data=get_field_data(user, self.user_fields)))
398 if profile_form is not None:
399 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
400 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
403 if not form.is_valid():
407 def account_required(self, view):
408 def inner(request, *args, **kwargs):
409 if not self.has_valid_account(request.user):
410 if not request.method == "POST":
411 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
412 return self.account_view(request, *args, **kwargs)
413 return view(request, *args, **kwargs)
415 inner = self.login_required(inner)
418 def post_register_confirm_redirect(self, request, node):
419 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
420 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
422 def email_change_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
424 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
426 assert uidb36 is not None and token is not None and email is not None
429 uid_int = base36_to_int(uidb36)
433 user = get_object_or_404(User, id=uid_int)
435 email = '@'.join(email.rsplit('+', 1))
437 if email == user.email:
438 # Then short-circuit.
441 if token_generator.check_token(user, email, token):
444 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
445 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))