Fixed login redirects
[philo.git] / contrib / waldo / models.py
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
21 import urlparse
22
23
24 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
25
26
27 def get_field_data(obj, fields):
28         if fields == None:
29                 fields = [field.name for field in obj._meta.fields if field.editable]
30         
31         return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
32
33
34 class LoginMultiView(MultiView):
35         """
36         Handles login, registration, and forgotten passwords. In other words, this
37         multiview provides exclusively view and methods related to usernames and
38         passwords.
39         """
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')
47         
48         @property
49         def urlpatterns(self):
50                 urlpatterns = patterns('',
51                         url(r'^login/$', self.login, name='login'),
52                         url(r'^logout/$', self.logout, name='logout'),
53                         
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'),
56                         
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')
59                 )
60                 
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'),
64                         )
65                 
66                 return urlpatterns
67         
68         def get_context(self, extra_dict=None):
69                 context = {}
70                 context.update(extra_dict or {})
71                 return context
72         
73         def display_login_page(self, request, message, node=None, extra_context=None):
74                 request.session.set_test_cookie()
75                 
76                 referrer = request.META.get('HTTP_REFERER', None)
77                 
78                 if referrer is not None:
79                         referrer = urlparse.urlparse(referrer)
80                         host = referrer[1]
81                         if host != request.get_host():
82                                 referrer = None
83                         else:
84                                 redirect = '%s?%s' % (referrer[2], referrer[4])
85                 
86                 if referrer is None:
87                         redirect = node.get_absolute_url()
88                 
89                 path = request.get_full_path()
90                 if redirect != path:
91                         if redirect is None:
92                                 redirect = '/'.join(path.split('/')[:-2])
93                         request.session['redirect'] = redirect
94                 
95                 if request.POST:
96                         form = LoginForm(request.POST)
97                 else:
98                         form = LoginForm()
99                 context = self.get_context({
100                         'message': message,
101                         'form': form
102                 })
103                 context.update(extra_context or {})
104                 return self.login_page.render_to_response(node, request, extra_context=context)
105         
106         def login(self, request, node=None, extra_context=None):
107                 """
108                 Displays the login form for the given HttpRequest.
109                 """
110                 if request.user.is_authenticated():
111                         return HttpResponseRedirect(node.get_absolute_url())
112                 
113                 context = self.get_context(extra_context)
114                 
115                 from django.contrib.auth.models import User
116                 
117                 # If this isn't already the login page, display it.
118                 if not request.POST.has_key(LOGIN_FORM_KEY):
119                         if request.POST:
120                                 message = _("Please log in again, because your session has expired.")
121                         else:
122                                 message = ""
123                         return self.display_login_page(request, message, node, context)
124
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)
129                 else:
130                         request.session.delete_test_cookie()
131                 
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)
136                 if user is None:
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.
140                                 try:
141                                         user = User.objects.get(email=username)
142                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
143                                         message = _("Usernames cannot contain the '@' character.")
144                                 else:
145                                         if user.check_password(password):
146                                                 message = _("Your e-mail address is not your username."
147                                                                         " Try '%s' instead.") % user.username
148                                         else:
149                                                 message = _("Usernames cannot contain the '@' character.")
150                         return self.display_login_page(request, message, node, context)
151
152                 # The user data is correct; log in the user in and continue.
153                 else:
154                         if user.is_active:
155                                 login(request, user)
156                                 try:
157                                         redirect = request.session.pop('redirect')
158                                 except KeyError:
159                                         redirect = node.get_absolute_url()
160                                 return HttpResponseRedirect(redirect)
161                         else:
162                                 return self.display_login_page(request, ERROR_MESSAGE, node, context)
163         login = never_cache(login)
164         
165         def logout(self, request):
166                 return auth_views.logout(request, request.META['HTTP_REFERER'])
167         
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)
174                 
175                 return inner
176         
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])
181         
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())
185                 
186                 if request.method == 'POST':
187                         form = PasswordResetForm(request.POST)
188                         if form.is_valid():
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('/'))
193                                         context = {
194                                                 'link': link,
195                                                 'username': user.username
196                                         }
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('')
200                 else:
201                         form = PasswordResetForm()
202                 
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)
206         
207         def password_reset_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
208                 """
209                 Checks that a given hash in a password reset link is valid. If so,
210                 displays the password set form.
211                 """
212                 assert uidb36 is not None and token is not None
213                 try:
214                         uid_int = base36_to_int(uidb36)
215                 except:
216                         raise Http404
217                 
218                 user = get_object_or_404(User, id=uid_int)
219                 
220                 if token_generator.check_token(user, token):
221                         if request.method == 'POST':
222                                 form = SetPasswordForm(user, request.POST)
223                                 
224                                 if form.is_valid():
225                                         form.save()
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('/')))
228                         else:
229                                 form = SetPasswordForm(user)
230                         
231                         context = self.get_context({'form': form})
232                         return self.password_set_page.render_to_response(node, request, extra_context=context)
233                 
234                 raise Http404
235         
236         def password_change(self, request, node=None, extra_context=None):
237                 if request.method == 'POST':
238                         form = PasswordChangeForm(request.user, request.POST)
239                         if form.is_valid():
240                                 form.save()
241                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
242                                 return HttpResponseRedirect('')
243                 else:
244                         form = PasswordChangeForm(request.user)
245                 
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)
249         
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())
253                 
254                 if request.method == 'POST':
255                         form = RegistrationForm(request.POST)
256                         if form.is_valid():
257                                 user = form.save()
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('/'))
261                                 context = {
262                                         'link': link
263                                 }
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())
267                 else:
268                         form = RegistrationForm()
269                 
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)
273         
274         def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
275                 """
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.
279                 """
280                 assert uidb36 is not None and token is not None
281                 try:
282                         uid_int = base36_to_int(uidb36)
283                 except:
284                         raise Http404
285                 
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
290                         try:
291                                 user.set_password('temp_password')
292                                 user.save()
293                                 authenticated_user = authenticate(username=user.username, password='temp_password')
294                                 login(request, authenticated_user)
295                         finally:
296                                 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
297                                 user.password = true_password
298                                 user.save()
299                         return self.post_register_confirm_redirect(request, node)
300                 
301                 raise Http404
302         
303         def post_register_confirm_redirect(self, request, node):
304                 return HttpResponseRedirect(node.get_absolute_url())
305         
306         class Meta:
307                 abstract = True
308
309
310 class AccountMultiView(LoginMultiView):
311         """
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
314         the account.
315         """
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
322         
323         @property
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')
329                 )
330                 return urlpatterns
331         
332         def get_account_forms(self):
333                 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
334                 
335                 if self.account_profile is None:
336                         profile_form = None
337                 else:
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'])
339                 
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
344         
345         def get_account_form_instances(self, user, data=None):
346                 form_instances = []
347                 user_form, profile_form = self.get_account_forms()
348                 if data is None:
349                         form_instances.append(user_form(instance=user))
350                         if profile_form:
351                                 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
352                 else:
353                         form_instances.append(user_form(data, instance=user))
354                         if profile_form:
355                                 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
356                 
357                 return form_instances
358         
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
363                         
364                         for form in form_instances:
365                                 if not form.is_valid():
366                                         break
367                         else:
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:
371                                         
372                                         request.user.email = current_email
373                                         
374                                         for form in form_instances:
375                                                 form.cleaned_data.pop('email', None)
376                                         
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('/'))
380                                         context = {
381                                                 'link': link
382                                         }
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)
385                                         
386                                 for form in form_instances:
387                                         form.save()
388                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
389                                 return HttpResponseRedirect('')
390                 else:
391                         form_instances = self.get_account_form_instances(request.user)
392                 
393                 context = self.get_context({
394                         'forms': form_instances
395                 })
396                 context.update(extra_context or {})
397                 return self.manage_account_page.render_to_response(node, request, extra_context=context)
398         
399         def has_valid_account(self, user):
400                 user_form, profile_form = self.get_account_forms()
401                 forms = []
402                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
403                 
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)))
407                 
408                 for form in forms:
409                         if not form.is_valid():
410                                 return False
411                 return True
412         
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)
420                 
421                 inner = self.login_required(inner)
422                 return inner
423         
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('/')))
427         
428         def email_change_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
429                 """
430                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
431                 """
432                 assert uidb36 is not None and token is not None and email is not None
433                 
434                 try:
435                         uid_int = base36_to_int(uidb36)
436                 except:
437                         raise Http404
438                 
439                 user = get_object_or_404(User, id=uid_int)
440                 
441                 email = '@'.join(email.rsplit('+', 1))
442                 
443                 if email == user.email:
444                         # Then short-circuit.
445                         raise Http404
446                 
447                 if token_generator.check_token(user, email, token):
448                         user.email = email
449                         user.save()
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('/')))
452                 
453                 raise Http404
454         
455         class Meta:
456                 abstract = True