Corrected Waldo AccountMultiView typo and improved View error reporting / TargetURL...
[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                 token = token_generator.make_token(user, *(token_args or []))
63                 kwargs = {
64                         'uidb36': int_to_base36(user.id),
65                         'token': token
66                 }
67                 kwargs.update(reverse_kwargs or {})
68                 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
69         
70         def display_login_page(self, request, message, extra_context=None):
71                 request.session.set_test_cookie()
72                 
73                 referrer = request.META.get('HTTP_REFERER', None)
74                 
75                 if referrer is not None:
76                         referrer = urlparse.urlparse(referrer)
77                         host = referrer[1]
78                         if host != request.get_host():
79                                 referrer = None
80                         else:
81                                 redirect = '%s?%s' % (referrer[2], referrer[4])
82                 
83                 if referrer is None:
84                         redirect = request.node.get_absolute_url()
85                 
86                 path = request.get_full_path()
87                 if redirect != path:
88                         if redirect is None:
89                                 redirect = '/'.join(path.split('/')[:-2])
90                         request.session['redirect'] = redirect
91                 
92                 if request.POST:
93                         form = LoginForm(request.POST)
94                 else:
95                         form = LoginForm()
96                 context = self.get_context()
97                 context.update(extra_context or {})
98                 context.update({
99                         'message': message,
100                         'form': form
101                 })
102                 return self.login_page.render_to_response(request, extra_context=context)
103         
104         def login(self, request, extra_context=None):
105                 """
106                 Displays the login form for the given HttpRequest.
107                 """
108                 if request.user.is_authenticated():
109                         return HttpResponseRedirect(request.node.get_absolute_url())
110                 
111                 context = self.get_context()
112                 context.update(extra_context or {})
113                 
114                 from django.contrib.auth.models import User
115                 
116                 # If this isn't already the login page, display it.
117                 if not request.POST.has_key(LOGIN_FORM_KEY):
118                         if request.POST:
119                                 message = _("Please log in again, because your session has expired.")
120                         else:
121                                 message = ""
122                         return self.display_login_page(request, message, context)
123
124                 # Check that the user accepts cookies.
125                 if not request.session.test_cookie_worked():
126                         message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
127                         return self.display_login_page(request, message, context)
128                 else:
129                         request.session.delete_test_cookie()
130                 
131                 # Check the password.
132                 username = request.POST.get('username', None)
133                 password = request.POST.get('password', None)
134                 user = authenticate(username=username, password=password)
135                 if user is None:
136                         message = ERROR_MESSAGE
137                         if username is not None and u'@' in username:
138                                 # Mistakenly entered e-mail address instead of username? Look it up.
139                                 try:
140                                         user = User.objects.get(email=username)
141                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
142                                         message = _("Usernames cannot contain the '@' character.")
143                                 else:
144                                         if user.check_password(password):
145                                                 message = _("Your e-mail address is not your username."
146                                                                         " Try '%s' instead.") % user.username
147                                         else:
148                                                 message = _("Usernames cannot contain the '@' character.")
149                         return self.display_login_page(request, message, context)
150
151                 # The user data is correct; log in the user in and continue.
152                 else:
153                         if user.is_active:
154                                 login(request, user)
155                                 try:
156                                         redirect = request.session.pop('redirect')
157                                 except KeyError:
158                                         redirect = request.node.get_absolute_url()
159                                 return HttpResponseRedirect(redirect)
160                         else:
161                                 return self.display_login_page(request, ERROR_MESSAGE, context)
162         login = never_cache(login)
163         
164         def logout(self, request):
165                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
166         
167         def login_required(self, view):
168                 def inner(request, *args, **kwargs):
169                         if not request.user.is_authenticated():
170                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
171                         return view(request, *args, **kwargs)
172                 
173                 return inner
174         
175         def send_confirmation_email(self, subject, email, page, extra_context):
176                 text_content = page.render_to_string(extra_context=extra_context)
177                 from_email = 'noreply@%s' % Site.objects.get_current().domain
178                 
179                 if page.template.mimetype == 'text/html':
180                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
181                         msg.attach_alternative(text_content, 'text/html')
182                         msg.send()
183                 else:
184                         send_mail(subject, text_content, from_email, [email])
185         
186         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
187                 if request.user.is_authenticated():
188                         return HttpResponseRedirect(request.node.get_absolute_url())
189                 
190                 if request.method == 'POST':
191                         form = PasswordResetForm(request.POST)
192                         if form.is_valid():
193                                 current_site = Site.objects.get_current()
194                                 for user in form.users_cache:
195                                         context = {
196                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
197                                                 'username': user.username
198                                         }
199                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
200                                         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)
201                                 return HttpResponseRedirect('')
202                 else:
203                         form = PasswordResetForm()
204                 
205                 context = self.get_context()
206                 context.update(extra_context or {})
207                 context.update({
208                         'form': form
209                 })
210                 return self.password_reset_page.render_to_response(request, extra_context=context)
211         
212         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
213                 """
214                 Checks that a given hash in a password reset link is valid. If so,
215                 displays the password set form.
216                 """
217                 assert uidb36 is not None and token is not None
218                 try:
219                         uid_int = base36_to_int(uidb36)
220                 except:
221                         raise Http404
222                 
223                 user = get_object_or_404(User, id=uid_int)
224                 
225                 if token_generator.check_token(user, token):
226                         if request.method == 'POST':
227                                 form = SetPasswordForm(user, request.POST)
228                                 
229                                 if form.is_valid():
230                                         form.save()
231                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
232                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
233                         else:
234                                 form = SetPasswordForm(user)
235                         
236                         context = self.get_context()
237                         context.update(extra_context or {})
238                         context.update({
239                                 'form': form
240                         })
241                         return self.password_set_page.render_to_response(request, extra_context=context)
242                 
243                 raise Http404
244         
245         def password_change(self, request, extra_context=None):
246                 if request.method == 'POST':
247                         form = PasswordChangeForm(request.user, request.POST)
248                         if form.is_valid():
249                                 form.save()
250                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
251                                 return HttpResponseRedirect('')
252                 else:
253                         form = PasswordChangeForm(request.user)
254                 
255                 context = self.get_context()
256                 context.update(extra_context or {})
257                 context.update({
258                         'form': form
259                 })
260                 return self.password_change_page.render_to_response(request, extra_context=context)
261         
262         def register(self, request, extra_context=None, token_generator=registration_token_generator):
263                 if request.user.is_authenticated():
264                         return HttpResponseRedirect(request.node.get_absolute_url())
265                 
266                 if request.method == 'POST':
267                         form = RegistrationForm(request.POST)
268                         if form.is_valid():
269                                 user = form.save()
270                                 context = {
271                                         'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
272                                 }
273                                 current_site = Site.objects.get_current()
274                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
275                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
276                                 return HttpResponseRedirect(request.node.get_absolute_url())
277                 else:
278                         form = RegistrationForm()
279                 
280                 context = self.get_context()
281                 context.update(extra_context or {})
282                 context.update({
283                         'form': form
284                 })
285                 return self.register_page.render_to_response(request, extra_context=context)
286         
287         def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
288                 """
289                 Checks that a given hash in a registration link is valid and activates
290                 the given account. If so, log them in and redirect to
291                 self.post_register_confirm_redirect.
292                 """
293                 assert uidb36 is not None and token is not None
294                 try:
295                         uid_int = base36_to_int(uidb36)
296                 except:
297                         raise Http404
298                 
299                 user = get_object_or_404(User, id=uid_int)
300                 if token_generator.check_token(user, token):
301                         user.is_active = True
302                         true_password = user.password
303                         temp_password = token_generator.make_token(user)
304                         try:
305                                 user.set_password(temp_password)
306                                 user.save()
307                                 authenticated_user = authenticate(username=user.username, password=temp_password)
308                                 login(request, authenticated_user)
309                         finally:
310                                 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
311                                 user.password = true_password
312                                 user.save()
313                         return self.post_register_confirm_redirect(request)
314                 
315                 raise Http404
316         
317         def post_register_confirm_redirect(self, request):
318                 return HttpResponseRedirect(request.node.get_absolute_url())
319         
320         class Meta:
321                 abstract = True
322
323
324 class AccountMultiView(LoginMultiView):
325         """
326         By default, the `account` consists of the first_name, last_name, and email fields
327         of the User model. Using a different account model is as simple as writing a form that
328         accepts a User instance as the first argument.
329         """
330         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
331         email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
332         account_form = UserAccountForm
333         
334         @property
335         def urlpatterns(self):
336                 urlpatterns = super(AccountMultiView, self).urlpatterns
337                 urlpatterns += patterns('',
338                         url(r'^account$', self.login_required(self.account_view), name='account'),
339                         url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
340                 )
341                 return urlpatterns
342         
343         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
344                 if request.method == 'POST':
345                         form = self.account_form(request.user, request.POST, request.FILES)
346                         
347                         if form.is_valid():
348                                 if 'email' in form.changed_data:
349                                         # ModelForms modify their instances in-place during validation,
350                                         # so reset the instance's email to its previous value here,
351                                         # then remove the new value from cleaned_data.
352                                         request.user.email = form.initial['email']
353                                         
354                                         email = form.cleaned_data.pop('email')
355                                         
356                                         context = {
357                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
358                                         }
359                                         current_site = Site.objects.get_current()
360                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
361                                         messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
362                                 
363                                 form.save()
364                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
365                                 return HttpResponseRedirect('')
366                 else:
367                         form = self.account_form(request.user)
368                 
369                 context = self.get_context()
370                 context.update(extra_context or {})
371                 context.update({
372                         'form': form
373                 })
374                 return self.manage_account_page.render_to_response(request, extra_context=context)
375         
376         def has_valid_account(self, user):
377                 form = self.account_form(user, {})
378                 form.data = form.initial
379                 return form.is_valid()
380         
381         def account_required(self, view):
382                 def inner(request, *args, **kwargs):
383                         if not self.has_valid_account(request.user):
384                                 if not request.method == "POST":
385                                         messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
386                                 return self.account_view(request, *args, **kwargs)
387                         return view(request, *args, **kwargs)
388                 
389                 inner = self.login_required(inner)
390                 return inner
391         
392         def post_register_confirm_redirect(self, request):
393                 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
394                 return HttpResponseRedirect(self.reverse('account', node=request.node))
395         
396         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
397                 """
398                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
399                 """
400                 assert uidb36 is not None and token is not None and email is not None
401                 
402                 try:
403                         uid_int = base36_to_int(uidb36)
404                 except:
405                         raise Http404
406                 
407                 user = get_object_or_404(User, id=uid_int)
408                 
409                 email = '@'.join(email.rsplit('+', 1))
410                 
411                 if email == user.email:
412                         # Then short-circuit.
413                         raise Http404
414                 
415                 if token_generator.check_token(user, email, token):
416                         user.email = email
417                         user.save()
418                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
419                         return HttpResponseRedirect(self.reverse('account', node=request.node))
420                 
421                 raise Http404
422         
423         class Meta:
424                 abstract = True