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 for user in form.users_cache:
184 current_site = Site.objects.get_current()
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_page')
311 user_fields = ['first_name', 'last_name', 'email']
312 required_user_fields = user_fields
313 account_profile = None
314 account_profile_fields = None
317 def urlpatterns(self):
318 urlpatterns = super(AccountMultiView, self).urlpatterns
319 urlpatterns += patterns('',
320 url(r'^account/$', self.login_required(self.account_view), name='account')
324 def get_account_forms(self):
325 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
327 if self.account_profile is None:
330 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'])
332 for field_name, field in user_form.base_fields.items():
333 if field_name in self.required_user_fields:
334 field.required = True
335 return user_form, profile_form
337 def get_account_form_instances(self, user, data=None):
339 user_form, profile_form = self.get_account_forms()
341 form_instances.append(user_form(instance=user))
343 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
345 form_instances.append(user_form(data, instance=user))
347 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
349 return form_instances
351 def account_view(self, request, node=None, extra_context=None):
352 if request.method == 'POST':
353 form_instances = self.get_account_form_instances(request.user, request.POST)
355 for form in form_instances:
356 if not form.is_valid():
359 for form in form_instances:
361 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
362 return HttpResponseRedirect('')
364 form_instances = self.get_account_form_instances(request.user)
366 context = self.get_context({
367 'forms': form_instances
369 context.update(extra_context or {})
370 return self.manage_account_page.render_to_response(node, request, extra_context=context)
372 def has_valid_account(self, user):
373 user_form, profile_form = self.get_account_forms()
375 forms.append(user_form(data=get_field_data(user, self.user_fields)))
377 if profile_form is not None:
378 profile = self.account_profile._default_manager.get_or_create(user=user)[0]
379 forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
382 if not form.is_valid():
386 def account_required(self, view):
387 def inner(request, *args, **kwargs):
388 if not self.has_valid_account(request.user):
389 messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.", fail_silently=True)
390 return self.account_view(request, *args, **kwargs)
391 return view(request, *args, **kwargs)
393 inner = self.login_required(inner)
396 def post_register_confirm_redirect(self, request, node):
397 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
398 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))