Merge branch 'master' of git://github.com/melinath/philo
[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_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, UserAccountForm
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 class LoginMultiView(MultiView):
28         """
29         Handles login, registration, and forgotten passwords. In other words, this
30         multiview provides exclusively view and methods related to usernames and
31         passwords.
32         """
33         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
34         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
35         password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
36         password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
37         password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
38         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
39         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
40         
41         @property
42         def urlpatterns(self):
43                 urlpatterns = patterns('',
44                         url(r'^login/$', self.login, name='login'),
45                         url(r'^logout/$', self.logout, name='logout'),
46                         
47                         url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
48                         url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.password_reset_confirm, name='password_reset_confirm'),
49                         
50                         url(r'^register/$', csrf_protect(self.register), name='register'),
51                         url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.register_confirm, name='register_confirm')
52                 )
53                 
54                 if self.password_change_page:
55                         urlpatterns += patterns('',
56                                 url(r'^password/change/$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
57                         )
58                 
59                 return urlpatterns
60         
61         def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
62                 current_site = Site.objects.get_current()
63                 token = token_generator.make_token(user, *(token_args or []))
64                 kwargs = {
65                         'uidb36': int_to_base36(user.id),
66                         'token': token
67                 }
68                 kwargs.update(reverse_kwargs or {})
69                 return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
70         
71         def display_login_page(self, request, message, extra_context=None):
72                 request.session.set_test_cookie()
73                 
74                 referrer = request.META.get('HTTP_REFERER', None)
75                 
76                 if referrer is not None:
77                         referrer = urlparse.urlparse(referrer)
78                         host = referrer[1]
79                         if host != request.get_host():
80                                 referrer = None
81                         else:
82                                 redirect = '%s?%s' % (referrer[2], referrer[4])
83                 
84                 if referrer is None:
85                         redirect = request.node.get_absolute_url()
86                 
87                 path = request.get_full_path()
88                 if redirect != path:
89                         if redirect is None:
90                                 redirect = '/'.join(path.split('/')[:-2])
91                         request.session['redirect'] = redirect
92                 
93                 if request.POST:
94                         form = LoginForm(request.POST)
95                 else:
96                         form = LoginForm()
97                 context = self.get_context()
98                 context.update(extra_context or {})
99                 context.update({
100                         'message': message,
101                         'form': form
102                 })
103                 return self.login_page.render_to_response(request, extra_context=context)
104         
105         def login(self, request, extra_context=None):
106                 """
107                 Displays the login form for the given HttpRequest.
108                 """
109                 if request.user.is_authenticated():
110                         return HttpResponseRedirect(request.node.get_absolute_url())
111                 
112                 context = self.get_context()
113                 context.update(extra_context or {})
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, 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, 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, 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 = request.node.get_absolute_url()
160                                 return HttpResponseRedirect(redirect)
161                         else:
162                                 return self.display_login_page(request, ERROR_MESSAGE, context)
163         login = never_cache(login)
164         
165         def logout(self, request):
166                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
167         
168         def login_required(self, view):
169                 def inner(request, *args, **kwargs):
170                         if not request.user.is_authenticated():
171                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
172                         return view(request, *args, **kwargs)
173                 
174                 return inner
175         
176         def send_confirmation_email(self, subject, email, page, extra_context):
177                 text_content = page.render_to_string(extra_context=extra_context)
178                 from_email = 'noreply@%s' % Site.objects.get_current().domain
179                 
180                 if page.template.mimetype == 'text/html':
181                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
182                         msg.attach_alternative(text_content, 'text/html')
183                         msg.send()
184                 else:
185                         send_mail(subject, text_content, from_email, [email])
186         
187         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
188                 if request.user.is_authenticated():
189                         return HttpResponseRedirect(request.node.get_absolute_url())
190                 
191                 if request.method == 'POST':
192                         form = PasswordResetForm(request.POST)
193                         if form.is_valid():
194                                 current_site = Site.objects.get_current()
195                                 for user in form.users_cache:
196                                         context = {
197                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
198                                                 'username': user.username
199                                         }
200                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
201                                         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)
202                                 return HttpResponseRedirect('')
203                 else:
204                         form = PasswordResetForm()
205                 
206                 context = self.get_context()
207                 context.update(extra_context or {})
208                 context.update({
209                         'form': form
210                 })
211                 return self.password_reset_page.render_to_response(request, extra_context=context)
212         
213         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
214                 """
215                 Checks that a given hash in a password reset link is valid. If so,
216                 displays the password set form.
217                 """
218                 assert uidb36 is not None and token is not None
219                 try:
220                         uid_int = base36_to_int(uidb36)
221                 except:
222                         raise Http404
223                 
224                 user = get_object_or_404(User, id=uid_int)
225                 
226                 if token_generator.check_token(user, token):
227                         if request.method == 'POST':
228                                 form = SetPasswordForm(user, request.POST)
229                                 
230                                 if form.is_valid():
231                                         form.save()
232                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
233                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
234                         else:
235                                 form = SetPasswordForm(user)
236                         
237                         context = self.get_context()
238                         context.update(extra_context or {})
239                         context.update({
240                                 'form': form
241                         })
242                         return self.password_set_page.render_to_response(request, extra_context=context)
243                 
244                 raise Http404
245         
246         def password_change(self, request, extra_context=None):
247                 if request.method == 'POST':
248                         form = PasswordChangeForm(request.user, request.POST)
249                         if form.is_valid():
250                                 form.save()
251                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
252                                 return HttpResponseRedirect('')
253                 else:
254                         form = PasswordChangeForm(request.user)
255                 
256                 context = self.get_context()
257                 context.update(extra_context or {})
258                 context.update({
259                         'form': form
260                 })
261                 return self.password_change_page.render_to_response(request, extra_context=context)
262         
263         def register(self, request, extra_context=None, token_generator=registration_token_generator):
264                 if request.user.is_authenticated():
265                         return HttpResponseRedirect(request.node.get_absolute_url())
266                 
267                 if request.method == 'POST':
268                         form = RegistrationForm(request.POST)
269                         if form.is_valid():
270                                 user = form.save()
271                                 context = {
272                                         'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
273                                 }
274                                 current_site = Site.objects.get_current()
275                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
276                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
277                                 return HttpResponseRedirect(request.node.get_absolute_url())
278                 else:
279                         form = RegistrationForm()
280                 
281                 context = self.get_context()
282                 context.update(extra_context or {})
283                 context.update({
284                         'form': form
285                 })
286                 return self.register_page.render_to_response(request, extra_context=context)
287         
288         def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
289                 """
290                 Checks that a given hash in a registration link is valid and activates
291                 the given account. If so, log them in and redirect to
292                 self.post_register_confirm_redirect.
293                 """
294                 assert uidb36 is not None and token is not None
295                 try:
296                         uid_int = base36_to_int(uidb36)
297                 except:
298                         raise Http404
299                 
300                 user = get_object_or_404(User, id=uid_int)
301                 if token_generator.check_token(user, token):
302                         user.is_active = True
303                         true_password = user.password
304                         temp_password = token_generator.make_token(user)
305                         try:
306                                 user.set_password(temp_password)
307                                 user.save()
308                                 authenticated_user = authenticate(username=user.username, password=temp_password)
309                                 login(request, authenticated_user)
310                         finally:
311                                 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
312                                 user.password = true_password
313                                 user.save()
314                         return self.post_register_confirm_redirect(request)
315                 
316                 raise Http404
317         
318         def post_register_confirm_redirect(self, request):
319                 return HttpResponseRedirect(request.node.get_absolute_url())
320         
321         class Meta:
322                 abstract = True
323
324
325 class AccountMultiView(LoginMultiView):
326         """
327         By default, the `account` consists of the first_name, last_name, and email fields
328         of the User model. Using a different account model is as simple as writing a form that
329         accepts a User instance as the first argument.
330         """
331         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
332         email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
333         account_form = UserAccountForm
334         
335         @property
336         def urlpatterns(self):
337                 urlpatterns = super(AccountMultiView, self).urlpatterns
338                 urlpatterns += patterns('',
339                         url(r'^account/$', self.login_required(self.account_view), name='account'),
340                         url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
341                 )
342                 return urlpatterns
343         
344         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
345                 if request.method == 'POST':
346                         form = self.account_form(request.user, request.POST, request.FILES)
347                         
348                         if form.is_valid():
349                                 if 'email' in form.changed_data:
350                                         # ModelForms modify their instances in-place during validation,
351                                         # so reset the instance's email to its previous value here,
352                                         # then remove the new value from cleaned_data.
353                                         request.user.email = form.initial['email']
354                                         
355                                         email = form.cleaned_data.pop('email')
356                                         
357                                         context = {
358                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
359                                         }
360                                         current_site = Site.objects.get_current()
361                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
362                                         messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
363                                 
364                                 form.save()
365                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
366                                 return HttpResponseRedirect('')
367                 else:
368                         form = self.account_form(request.user)
369                 
370                 context = self.get_context()
371                 context.update(extra_context or {})
372                 context.update({
373                         'form': form
374                 })
375                 return self.manage_account_page.render_to_response(request, extra_context=context)
376         
377         def has_valid_account(self, user):
378                 form = self.account_form(user, {})
379                 form.data = form.initial
380                 return form.is_valid()
381         
382         def account_required(self, view):
383                 def inner(request, *args, **kwargs):
384                         if not self.has_valid_account(request.user):
385                                 if not request.method == "POST":
386                                         messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
387                                 return self.account_view(request, *args, **kwargs)
388                         return view(request, *args, **kwargs)
389                 
390                 inner = self.login_required(inner)
391                 return inner
392         
393         def post_register_confirm_redirect(self, request):
394                 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
395                 return HttpResponseRedirect(self.reverse('account', node=request.node))
396         
397         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
398                 """
399                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
400                 """
401                 assert uidb36 is not None and token is not None and email is not None
402                 
403                 try:
404                         uid_int = base36_to_int(uidb36)
405                 except:
406                         raise Http404
407                 
408                 user = get_object_or_404(User, id=uid_int)
409                 
410                 email = '@'.join(email.rsplit('+', 1))
411                 
412                 if email == user.email:
413                         # Then short-circuit.
414                         raise Http404
415                 
416                 if token_generator.check_token(user, email, token):
417                         user.email = email
418                         user.save()
419                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
420                         return HttpReponseRedirect(self.reverse('account', node=request.node))
421                 
422                 raise Http404
423         
424         class Meta:
425                 abstract = True