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