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 get_context(self, extra_dict=None):
71 context.update(extra_dict or {})
74 def display_login_page(self, request, message, extra_context=None):
75 request.session.set_test_cookie()
77 referrer = request.META.get('HTTP_REFERER', None)
79 if referrer is not None:
80 referrer = urlparse.urlparse(referrer)
82 if host != request.get_host():
85 redirect = '%s?%s' % (referrer[2], referrer[4])
88 redirect = request.node.get_absolute_url()
90 path = request.get_full_path()
93 redirect = '/'.join(path.split('/')[:-2])
94 request.session['redirect'] = redirect
97 form = LoginForm(request.POST)
100 context = self.get_context({
104 context.update(extra_context or {})
105 return self.login_page.render_to_response(request, extra_context=context)
107 def login(self, request, extra_context=None):
109 Displays the login form for the given HttpRequest.
111 if request.user.is_authenticated():
112 return HttpResponseRedirect(request.node.get_absolute_url())
114 context = self.get_context(extra_context)
116 from django.contrib.auth.models import User
118 # If this isn't already the login page, display it.
119 if not request.POST.has_key(LOGIN_FORM_KEY):
121 message = _("Please log in again, because your session has expired.")
124 return self.display_login_page(request, message, context)
126 # Check that the user accepts cookies.
127 if not request.session.test_cookie_worked():
128 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
129 return self.display_login_page(request, message, context)
131 request.session.delete_test_cookie()
133 # Check the password.
134 username = request.POST.get('username', None)
135 password = request.POST.get('password', None)
136 user = authenticate(username=username, password=password)
138 message = ERROR_MESSAGE
139 if username is not None and u'@' in username:
140 # Mistakenly entered e-mail address instead of username? Look it up.
142 user = User.objects.get(email=username)
143 except (User.DoesNotExist, User.MultipleObjectsReturned):
144 message = _("Usernames cannot contain the '@' character.")
146 if user.check_password(password):
147 message = _("Your e-mail address is not your username."
148 " Try '%s' instead.") % user.username
150 message = _("Usernames cannot contain the '@' character.")
151 return self.display_login_page(request, message, context)
153 # The user data is correct; log in the user in and continue.
158 redirect = request.session.pop('redirect')
160 redirect = request.node.get_absolute_url()
161 return HttpResponseRedirect(redirect)
163 return self.display_login_page(request, ERROR_MESSAGE, context)
164 login = never_cache(login)
166 def logout(self, request):
167 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
169 def login_required(self, view):
170 def inner(request, *args, **kwargs):
171 if not request.user.is_authenticated():
172 login_url = reverse('login', urlconf=self).strip('/')
173 return HttpResponseRedirect('%s%s/' % (request.node.get_absolute_url(), login_url))
174 return view(request, *args, **kwargs)
178 def send_confirmation_email(self, subject, email, page, extra_context):
179 text_content = page.render_to_string(extra_context=extra_context)
180 from_email = 'noreply@%s' % Site.objects.get_current().domain
182 if page.template.mimetype == 'text/html':
183 msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
184 msg.attach_alternative(text_content, 'text/html')
187 send_mail(subject, text_content, from_email, [email])
189 def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
190 if request.user.is_authenticated():
191 return HttpResponseRedirect(request.node.get_absolute_url())
193 if request.method == 'POST':
194 form = PasswordResetForm(request.POST)
196 current_site = Site.objects.get_current()
197 for user in form.users_cache:
198 token = token_generator.make_token(user)
199 link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
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({'form': form})
211 context.update(extra_context or {})
212 return self.password_reset_page.render_to_response(request, extra_context=context)
214 def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
216 Checks that a given hash in a password reset link is valid. If so,
217 displays the password set form.
219 assert uidb36 is not None and token is not None
221 uid_int = base36_to_int(uidb36)
225 user = get_object_or_404(User, id=uid_int)
227 if token_generator.check_token(user, token):
228 if request.method == 'POST':
229 form = SetPasswordForm(user, request.POST)
233 messages.add_message(request, messages.SUCCESS, "Password reset successful.")
234 return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
236 form = SetPasswordForm(user)
238 context = self.get_context({'form': form})
239 return self.password_set_page.render_to_response(request, extra_context=context)
243 def password_change(self, request, extra_context=None):
244 if request.method == 'POST':
245 form = PasswordChangeForm(request.user, request.POST)
248 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
249 return HttpResponseRedirect('')
251 form = PasswordChangeForm(request.user)
253 context = self.get_context({'form': form})
254 context.update(extra_context or {})
255 return self.password_change_page.render_to_response(request, extra_context=context)
257 def register(self, request, extra_context=None, token_generator=registration_token_generator):
258 if request.user.is_authenticated():
259 return HttpResponseRedirect(request.node.get_absolute_url())
261 if request.method == 'POST':
262 form = RegistrationForm(request.POST)
265 current_site = Site.objects.get_current()
266 token = token_generator.make_token(user)
267 link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
271 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
272 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
273 return HttpResponseRedirect(request.node.get_absolute_url())
275 form = RegistrationForm()
277 context = self.get_context({'form': form})
278 context.update(extra_context or {})
279 return self.register_page.render_to_response(request, extra_context=context)
281 def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
283 Checks that a given hash in a registration link is valid and activates
284 the given account. If so, log them in and redirect to
285 self.post_register_confirm_redirect.
287 assert uidb36 is not None and token is not None
289 uid_int = base36_to_int(uidb36)
293 user = get_object_or_404(User, id=uid_int)
294 if token_generator.check_token(user, token):
295 user.is_active = True
296 true_password = user.password
298 user.set_password('temp_password')
300 authenticated_user = authenticate(username=user.username, password='temp_password')
301 login(request, authenticated_user)
303 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
304 user.password = true_password
306 return self.post_register_confirm_redirect(request)
310 def post_register_confirm_redirect(self, request):
311 return HttpResponseRedirect(request.node.get_absolute_url())
317 class AccountMultiView(LoginMultiView):
319 Subclasses may define an account_profile model, fields from the User model
320 to include in the account, and fields from the account profile to use in
323 manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
324 email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
325 user_fields = ['first_name', 'last_name', 'email']
326 required_user_fields = user_fields
327 account_profile = None
328 account_profile_fields = None
331 def urlpatterns(self):
332 urlpatterns = super(AccountMultiView, self).urlpatterns
333 urlpatterns += patterns('',
334 url(r'^account/$', self.login_required(self.account_view), name='account'),
335 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
339 def get_account_forms(self):
340 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
342 if self.account_profile is None:
345 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'])
347 for field_name, field in user_form.base_fields.items():
348 if field_name in self.required_user_fields:
349 field.required = True
350 return user_form, profile_form
352 def get_account_form_instances(self, user, data=None):
354 user_form, profile_form = self.get_account_forms()
356 form_instances.append(user_form(instance=user))
358 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
360 form_instances.append(user_form(data, instance=user))
362 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
364 return form_instances
366 def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
367 if request.method == 'POST':
368 form_instances = self.get_account_form_instances(request.user, request.POST)
369 current_email = request.user.email
371 for form in form_instances:
372 if not form.is_valid():
375 # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
376 email = request.user.email
377 if current_email != email:
379 request.user.email = current_email
381 for form in form_instances:
382 form.cleaned_data.pop('email', None)
384 current_site = Site.objects.get_current()
385 token = token_generator.make_token(request.user, email)
386 link = 'http://%s/%s/%s/' % (current_site.domain, request.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('/'))
390 self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
391 messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
393 for form in form_instances:
395 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
396 return HttpResponseRedirect('')
398 form_instances = self.get_account_form_instances(request.user)
400 context = self.get_context({
401 'forms': form_instances
403 context.update(extra_context or {})
404 return self.manage_account_page.render_to_response(request, extra_context=context)
406 def has_valid_account(self, user):
407 user_form, profile_form = self.get_account_forms()
409 forms.append(user_form(data=get_field_data(user, self.user_fields)))
411 if profile_form is not None:
412 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
413 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
416 if not form.is_valid():
420 def account_required(self, view):
421 def inner(request, *args, **kwargs):
422 if not self.has_valid_account(request.user):
423 if not request.method == "POST":
424 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
425 return self.account_view(request, *args, **kwargs)
426 return view(request, *args, **kwargs)
428 inner = self.login_required(inner)
431 def post_register_confirm_redirect(self, request):
432 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
433 return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
435 def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
437 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
439 assert uidb36 is not None and token is not None and email is not None
442 uid_int = base36_to_int(uidb36)
446 user = get_object_or_404(User, id=uid_int)
448 email = '@'.join(email.rsplit('+', 1))
450 if email == user.email:
451 # Then short-circuit.
454 if token_generator.check_token(user, email, token):
457 messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
458 return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))