f47dbd6e44c07aa1ee5fd48c67f4c2d5badee01f
[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 EmailMultiAlternatives, send_mail
10 from django.db import models
11 from django.http import Http404, HttpResponseRedirect
12 from django.shortcuts import render_to_response, get_object_or_404
13 from django.template.defaultfilters import striptags
14 from django.utils.http import int_to_base36, base36_to_int
15 from django.utils.translation import 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 WaldoAuthenticationForm, RegistrationForm, UserAccountForm
20 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
21 import urlparse
22
23
24 class LoginMultiView(MultiView):
25         """
26         Handles login, registration, and forgotten passwords. In other words, this
27         multiview provides exclusively view and methods related to usernames and
28         passwords.
29         """
30         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
31         login_form = WaldoAuthenticationForm
32         
33         @property
34         def urlpatterns(self):
35                 return patterns('',
36                         url(r'^login$', self.login, name='login'),
37                         url(r'^logout$', self.logout, name='logout'),
38                 )
39         
40         def set_requirement_redirect(self, request, redirect=None):
41                 "Figure out where someone should end up after landing on a `requirement` page like the login page."
42                 if redirect is not None:
43                         pass
44                 elif 'requirement_redirect' in request.session:
45                         return
46                 else:
47                         referrer = request.META.get('HTTP_REFERER', None)
48                 
49                         if referrer is not None:
50                                 referrer = urlparse.urlparse(referrer)
51                                 host = referrer[1]
52                                 if host != request.get_host():
53                                         referrer = None
54                                 else:
55                                         redirect = '%s?%s' % (referrer[2], referrer[4])
56                 
57                         path = request.get_full_path()
58                         if referrer is None or redirect == path:
59                                 # Default to the index page if we can't find a referrer or
60                                 # if we'd otherwise redirect to where we already are.
61                                 redirect = request.node.get_absolute_url()
62                 
63                 request.session['requirement_redirect'] = redirect
64         
65         def get_requirement_redirect(self, request, default=None):
66                 redirect = request.session.pop('requirement_redirect', None)
67                 # Security checks a la django.contrib.auth.views.login
68                 if not redirect or ' ' in redirect:
69                         redirect = default
70                 else:
71                         netloc = urlparse.urlparse(redirect)[1]
72                         if netloc and netloc != request.get_host():
73                                 redirect = default
74                 if redirect is None:
75                         redirect = request.node.get_absolute_url()
76                 return redirect
77         
78         @never_cache
79         def login(self, request, extra_context=None):
80                 """
81                 Displays the login form for the given HttpRequest.
82                 """
83                 self.set_requirement_redirect(request)
84                 
85                 # Redirect already-authenticated users to the index page.
86                 if request.user.is_authenticated():
87                         messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
88                         return HttpResponseRedirect(self.get_requirement_redirect(request))
89                 
90                 if request.method == 'POST':
91                         form = self.login_form(request=request, data=request.POST)
92                         if form.is_valid():
93                                 redirect = self.get_requirement_redirect(request)
94                                 login(request, form.get_user())
95                                 
96                                 if request.session.test_cookie_worked():
97                                         request.session.delete_test_cookie()
98                                 
99                                 return HttpResponseRedirect(redirect)
100                 else:
101                         form = self.login_form()
102                 
103                 request.session.set_test_cookie()
104                 
105                 context = self.get_context()
106                 context.update(extra_context or {})
107                 context.update({
108                         'form': form
109                 })
110                 return self.login_page.render_to_response(request, extra_context=context)
111         
112         @never_cache
113         def logout(self, request, extra_context=None):
114                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
115         
116         def login_required(self, view):
117                 def inner(request, *args, **kwargs):
118                         if not request.user.is_authenticated():
119                                 self.set_requirement_redirect(request, redirect=request.path)
120                                 if request.POST:
121                                         messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
122                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
123                         return view(request, *args, **kwargs)
124                 
125                 return inner
126         
127         class Meta:
128                 abstract = True
129
130
131 class PasswordMultiView(LoginMultiView):
132         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
133         password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
134         password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
135         password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
136         
137         @property
138         def urlpatterns(self):
139                 urlpatterns = super(PasswordMultiView, self).urlpatterns
140                 
141                 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
142                         urlpatterns += patterns('',
143                                 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
144                                 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
145                         )
146                 
147                 if self.password_change_page:
148                         urlpatterns += patterns('',
149                                 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
150                         )
151                 return urlpatterns
152         
153         def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
154                 token = token_generator.make_token(user, *(token_args or []))
155                 kwargs = {
156                         'uidb36': int_to_base36(user.id),
157                         'token': token
158                 }
159                 kwargs.update(reverse_kwargs or {})
160                 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
161         
162         def send_confirmation_email(self, subject, email, page, extra_context):
163                 text_content = page.render_to_string(extra_context=extra_context)
164                 from_email = 'noreply@%s' % Site.objects.get_current().domain
165                 
166                 if page.template.mimetype == 'text/html':
167                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
168                         msg.attach_alternative(text_content, 'text/html')
169                         msg.send()
170                 else:
171                         send_mail(subject, text_content, from_email, [email])
172         
173         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
174                 if request.user.is_authenticated():
175                         return HttpResponseRedirect(request.node.get_absolute_url())
176                 
177                 if request.method == 'POST':
178                         form = PasswordResetForm(request.POST)
179                         if form.is_valid():
180                                 current_site = Site.objects.get_current()
181                                 for user in form.users_cache:
182                                         context = {
183                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
184                                                 'username': user.username
185                                         }
186                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
187                                         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)
188                                 return HttpResponseRedirect('')
189                 else:
190                         form = PasswordResetForm()
191                 
192                 context = self.get_context()
193                 context.update(extra_context or {})
194                 context.update({
195                         'form': form
196                 })
197                 return self.password_reset_page.render_to_response(request, extra_context=context)
198         
199         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
200                 """
201                 Checks that a given hash in a password reset link is valid. If so,
202                 displays the password set form.
203                 """
204                 assert uidb36 is not None and token is not None
205                 try:
206                         uid_int = base36_to_int(uidb36)
207                 except:
208                         raise Http404
209                 
210                 user = get_object_or_404(User, id=uid_int)
211                 
212                 if token_generator.check_token(user, token):
213                         if request.method == 'POST':
214                                 form = SetPasswordForm(user, request.POST)
215                                 
216                                 if form.is_valid():
217                                         form.save()
218                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
219                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
220                         else:
221                                 form = SetPasswordForm(user)
222                         
223                         context = self.get_context()
224                         context.update(extra_context or {})
225                         context.update({
226                                 'form': form
227                         })
228                         return self.password_set_page.render_to_response(request, extra_context=context)
229                 
230                 raise Http404
231         
232         def password_change(self, request, extra_context=None):
233                 if request.method == 'POST':
234                         form = PasswordChangeForm(request.user, request.POST)
235                         if form.is_valid():
236                                 form.save()
237                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
238                                 return HttpResponseRedirect('')
239                 else:
240                         form = PasswordChangeForm(request.user)
241                 
242                 context = self.get_context()
243                 context.update(extra_context or {})
244                 context.update({
245                         'form': form
246                 })
247                 return self.password_change_page.render_to_response(request, extra_context=context)
248         
249         class Meta:
250                 abstract = True
251
252
253 class RegistrationMultiView(PasswordMultiView):
254         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
255         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
256         
257         @property
258         def urlpatterns(self):
259                 urlpatterns = super(RegistrationMultiView, self).urlpatterns
260                 if self.register_page and self.register_confirmation_email:
261                         urlpatterns += patterns('',
262                                 url(r'^register$', csrf_protect(self.register), name='register'),
263                                 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
264                         )
265                 return urlpatterns
266         
267         def register(self, request, extra_context=None, token_generator=registration_token_generator):
268                 if request.user.is_authenticated():
269                         return HttpResponseRedirect(request.node.get_absolute_url())
270                 
271                 if request.method == 'POST':
272                         form = RegistrationForm(request.POST)
273                         if form.is_valid():
274                                 user = form.save()
275                                 context = {
276                                         'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
277                                 }
278                                 current_site = Site.objects.get_current()
279                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
280                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
281                                 return HttpResponseRedirect(request.node.get_absolute_url())
282                 else:
283                         form = RegistrationForm()
284                 
285                 context = self.get_context()
286                 context.update(extra_context or {})
287                 context.update({
288                         'form': form
289                 })
290                 return self.register_page.render_to_response(request, extra_context=context)
291         
292         def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
293                 """
294                 Checks that a given hash in a registration link is valid and activates
295                 the given account. If so, log them in and redirect to
296                 self.post_register_confirm_redirect.
297                 """
298                 assert uidb36 is not None and token is not None
299                 try:
300                         uid_int = base36_to_int(uidb36)
301                 except:
302                         raise Http404
303                 
304                 user = get_object_or_404(User, id=uid_int)
305                 if token_generator.check_token(user, token):
306                         user.is_active = True
307                         true_password = user.password
308                         temp_password = token_generator.make_token(user)
309                         try:
310                                 user.set_password(temp_password)
311                                 user.save()
312                                 authenticated_user = authenticate(username=user.username, password=temp_password)
313                                 login(request, authenticated_user)
314                         finally:
315                                 # if anything goes wrong, do our best make sure that the true password is restored.
316                                 user.password = true_password
317                                 user.save()
318                         return self.post_register_confirm_redirect(request)
319                 
320                 raise Http404
321         
322         def post_register_confirm_redirect(self, request):
323                 return HttpResponseRedirect(request.node.get_absolute_url())
324         
325         class Meta:
326                 abstract = True
327
328
329 class AccountMultiView(RegistrationMultiView):
330         """
331         By default, the `account` consists of the first_name, last_name, and email fields
332         of the User model. Using a different account model is as simple as writing a form that
333         accepts a User instance as the first argument.
334         """
335         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
336         email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
337         
338         account_form = UserAccountForm
339         
340         @property
341         def urlpatterns(self):
342                 urlpatterns = super(AccountMultiView, self).urlpatterns
343                 if self.manage_account_page:
344                         urlpatterns += patterns('',
345                                 url(r'^account$', self.login_required(self.account_view), name='account'),
346                         )
347                 if self.email_change_confirmation_email:
348                         urlpatterns += patterns('',
349                                 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
350                         )
351                 return urlpatterns
352         
353         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
354                 if request.method == 'POST':
355                         form = self.account_form(request.user, request.POST, request.FILES)
356                         
357                         if form.is_valid():
358                                 message = "Account information saved."
359                                 redirect = self.get_requirement_redirect(request, default='')
360                                 if 'email' in form.changed_data and self.email_change_confirmation_email:
361                                         # ModelForms modify their instances in-place during
362                                         # validation, so reset the instance's email to its
363                                         # previous value here, then remove the new value
364                                         # from cleaned_data. We only do this if an email
365                                         # change confirmation email is available.
366                                         request.user.email = form.initial['email']
367                                         
368                                         email = form.cleaned_data.pop('email')
369                                         
370                                         context = {
371                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
372                                         }
373                                         current_site = Site.objects.get_current()
374                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
375                                         
376                                         message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
377                                         if not request.user.email:
378                                                 message += " You will need to confirm the email before accessing pages that require a valid account."
379                                                 redirect = ''
380                                 
381                                 form.save()
382                                 
383                                 if redirect != '':
384                                         message += " Here you go!"
385                                 
386                                 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
387                                 return HttpResponseRedirect(redirect)
388                 else:
389                         form = self.account_form(request.user)
390                 
391                 context = self.get_context()
392                 context.update(extra_context or {})
393                 context.update({
394                         'form': form
395                 })
396                 return self.manage_account_page.render_to_response(request, extra_context=context)
397         
398         def has_valid_account(self, user):
399                 form = self.account_form(user, {})
400                 form.data = form.initial
401                 return form.is_valid()
402         
403         def account_required(self, view):
404                 def inner(request, *args, **kwargs):
405                         if not self.has_valid_account(request.user):
406                                 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
407                                 if self.manage_account_page:
408                                         self.set_requirement_redirect(request, redirect=request.path)
409                                         redirect = self.reverse('account', node=request.node)
410                                 else:
411                                         redirect = node.get_absolute_url()
412                                 return HttpResponseRedirect(redirect)
413                         return view(request, *args, **kwargs)
414                 
415                 inner = self.login_required(inner)
416                 return inner
417         
418         def post_register_confirm_redirect(self, request):
419                 if self.manage_account_page:
420                         messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
421                         return HttpResponseRedirect(self.reverse('account', node=request.node))
422                 return super(AccountMultiView, self).post_register_confirm_redirect(request)
423         
424         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
425                 """
426                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
427                 """
428                 assert uidb36 is not None and token is not None and email is not None
429                 
430                 try:
431                         uid_int = base36_to_int(uidb36)
432                 except:
433                         raise Http404
434                 
435                 user = get_object_or_404(User, id=uid_int)
436                 
437                 email = '@'.join(email.rsplit('+', 1))
438                 
439                 if email == user.email:
440                         # Then short-circuit.
441                         raise Http404
442                 
443                 if token_generator.check_token(user, email, token):
444                         user.email = email
445                         user.save()
446                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
447                         if self.manage_account_page:
448                                 redirect = self.reverse('account', node=request.node)
449                         else:
450                                 redirect = request.node.get_absolute_url()
451                         return HttpResponseRedirect(redirect)
452                 
453                 raise Http404
454         
455         class Meta:
456                 abstract = True