e3dd07954c7d03c1deb1414cd1570009b482a8ce
[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 get_context(self):
72                 """Hook for providing instance-specific context - such as the value of a Field - to all views."""
73                 return {}
74         
75         def display_login_page(self, request, message, extra_context=None):
76                 request.session.set_test_cookie()
77                 
78                 referrer = request.META.get('HTTP_REFERER', None)
79                 
80                 if referrer is not None:
81                         referrer = urlparse.urlparse(referrer)
82                         host = referrer[1]
83                         if host != request.get_host():
84                                 referrer = None
85                         else:
86                                 redirect = '%s?%s' % (referrer[2], referrer[4])
87                 
88                 if referrer is None:
89                         redirect = request.node.get_absolute_url()
90                 
91                 path = request.get_full_path()
92                 if redirect != path:
93                         if redirect is None:
94                                 redirect = '/'.join(path.split('/')[:-2])
95                         request.session['redirect'] = redirect
96                 
97                 if request.POST:
98                         form = LoginForm(request.POST)
99                 else:
100                         form = LoginForm()
101                 context = self.get_context()
102                 context.update(extra_context or {})
103                 context.update({
104                         'message': message,
105                         'form': form
106                 })
107                 return self.login_page.render_to_response(request, extra_context=context)
108         
109         def login(self, request, extra_context=None):
110                 """
111                 Displays the login form for the given HttpRequest.
112                 """
113                 if request.user.is_authenticated():
114                         return HttpResponseRedirect(request.node.get_absolute_url())
115                 
116                 context = self.get_context()
117                 context.update(extra_context or {})
118                 
119                 from django.contrib.auth.models import User
120                 
121                 # If this isn't already the login page, display it.
122                 if not request.POST.has_key(LOGIN_FORM_KEY):
123                         if request.POST:
124                                 message = _("Please log in again, because your session has expired.")
125                         else:
126                                 message = ""
127                         return self.display_login_page(request, message, context)
128
129                 # Check that the user accepts cookies.
130                 if not request.session.test_cookie_worked():
131                         message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
132                         return self.display_login_page(request, message, context)
133                 else:
134                         request.session.delete_test_cookie()
135                 
136                 # Check the password.
137                 username = request.POST.get('username', None)
138                 password = request.POST.get('password', None)
139                 user = authenticate(username=username, password=password)
140                 if user is None:
141                         message = ERROR_MESSAGE
142                         if username is not None and u'@' in username:
143                                 # Mistakenly entered e-mail address instead of username? Look it up.
144                                 try:
145                                         user = User.objects.get(email=username)
146                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
147                                         message = _("Usernames cannot contain the '@' character.")
148                                 else:
149                                         if user.check_password(password):
150                                                 message = _("Your e-mail address is not your username."
151                                                                         " Try '%s' instead.") % user.username
152                                         else:
153                                                 message = _("Usernames cannot contain the '@' character.")
154                         return self.display_login_page(request, message, context)
155
156                 # The user data is correct; log in the user in and continue.
157                 else:
158                         if user.is_active:
159                                 login(request, user)
160                                 try:
161                                         redirect = request.session.pop('redirect')
162                                 except KeyError:
163                                         redirect = request.node.get_absolute_url()
164                                 return HttpResponseRedirect(redirect)
165                         else:
166                                 return self.display_login_page(request, ERROR_MESSAGE, context)
167         login = never_cache(login)
168         
169         def logout(self, request):
170                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
171         
172         def login_required(self, view):
173                 def inner(request, *args, **kwargs):
174                         if not request.user.is_authenticated():
175                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
176                         return view(request, *args, **kwargs)
177                 
178                 return inner
179         
180         def send_confirmation_email(self, subject, email, page, extra_context):
181                 text_content = page.render_to_string(extra_context=extra_context)
182                 from_email = 'noreply@%s' % Site.objects.get_current().domain
183                 
184                 if page.template.mimetype == 'text/html':
185                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
186                         msg.attach_alternative(text_content, 'text/html')
187                         msg.send()
188                 else:
189                         send_mail(subject, text_content, from_email, [email])
190         
191         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
192                 if request.user.is_authenticated():
193                         return HttpResponseRedirect(request.node.get_absolute_url())
194                 
195                 if request.method == 'POST':
196                         form = PasswordResetForm(request.POST)
197                         if form.is_valid():
198                                 current_site = Site.objects.get_current()
199                                 for user in form.users_cache:
200                                         context = {
201                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
202                                                 'username': user.username
203                                         }
204                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
205                                         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)
206                                 return HttpResponseRedirect('')
207                 else:
208                         form = PasswordResetForm()
209                 
210                 context = self.get_context()
211                 context.update(extra_context or {})
212                 context.update({
213                         'form': form
214                 })
215                 return self.password_reset_page.render_to_response(request, extra_context=context)
216         
217         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
218                 """
219                 Checks that a given hash in a password reset link is valid. If so,
220                 displays the password set form.
221                 """
222                 assert uidb36 is not None and token is not None
223                 try:
224                         uid_int = base36_to_int(uidb36)
225                 except:
226                         raise Http404
227                 
228                 user = get_object_or_404(User, id=uid_int)
229                 
230                 if token_generator.check_token(user, token):
231                         if request.method == 'POST':
232                                 form = SetPasswordForm(user, request.POST)
233                                 
234                                 if form.is_valid():
235                                         form.save()
236                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
237                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
238                         else:
239                                 form = SetPasswordForm(user)
240                         
241                         context = self.get_context()
242                         context.update(extra_context or {})
243                         context.update({
244                                 'form': form
245                         })
246                         return self.password_set_page.render_to_response(request, extra_context=context)
247                 
248                 raise Http404
249         
250         def password_change(self, request, extra_context=None):
251                 if request.method == 'POST':
252                         form = PasswordChangeForm(request.user, request.POST)
253                         if form.is_valid():
254                                 form.save()
255                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
256                                 return HttpResponseRedirect('')
257                 else:
258                         form = PasswordChangeForm(request.user)
259                 
260                 context = self.get_context()
261                 context.update(extra_context or {})
262                 context.update({
263                         'form': form
264                 })
265                 return self.password_change_page.render_to_response(request, extra_context=context)
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, ABSOLUTELY 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(LoginMultiView):
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')
336         email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
337         account_form = UserAccountForm
338         
339         @property
340         def urlpatterns(self):
341                 urlpatterns = super(AccountMultiView, self).urlpatterns
342                 urlpatterns += patterns('',
343                         url(r'^account/$', self.login_required(self.account_view), name='account'),
344                         url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
345                 )
346                 return urlpatterns
347         
348         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
349                 if request.method == 'POST':
350                         form = self.account_form(request.user, request.POST, request.FILES)
351                         
352                         if form.is_valid():
353                                 if 'email' in form.changed_data:
354                                         # ModelForms modify their instances in-place during validation,
355                                         # so reset the instance's email to its previous value here,
356                                         # then remove the new value from cleaned_data.
357                                         request.user.email = form.initial['email']
358                                         
359                                         email = form.cleaned_data.pop('email')
360                                         
361                                         context = {
362                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
363                                         }
364                                         current_site = Site.objects.get_current()
365                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
366                                         messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
367                                 
368                                 form.save()
369                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
370                                 return HttpResponseRedirect('')
371                 else:
372                         form = self.account_form(request.user)
373                 
374                 context = self.get_context()
375                 context.update(extra_context or {})
376                 context.update({
377                         'form': form
378                 })
379                 return self.manage_account_page.render_to_response(request, extra_context=context)
380         
381         def has_valid_account(self, user):
382                 user_form, profile_form = self.get_account_forms()
383                 forms = []
384                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
385                 
386                 if profile_form is not None:
387                         profile = self.account_profile._default_manager.get_or_create(user=user)[0]
388                         forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
389                 
390                 for form in forms:
391                         if not form.is_valid():
392                                 return False
393                 return True
394         
395         def account_required(self, view):
396                 def inner(request, *args, **kwargs):
397                         if not self.has_valid_account(request.user):
398                                 if not request.method == "POST":
399                                         messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
400                                 return self.account_view(request, *args, **kwargs)
401                         return view(request, *args, **kwargs)
402                 
403                 inner = self.login_required(inner)
404                 return inner
405         
406         def post_register_confirm_redirect(self, request):
407                 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
408                 return HttpResponseRedirect(self.reverse('account', node=request.node))
409         
410         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
411                 """
412                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
413                 """
414                 assert uidb36 is not None and token is not None and email is not None
415                 
416                 try:
417                         uid_int = base36_to_int(uidb36)
418                 except:
419                         raise Http404
420                 
421                 user = get_object_or_404(User, id=uid_int)
422                 
423                 email = '@'.join(email.rsplit('+', 1))
424                 
425                 if email == user.email:
426                         # Then short-circuit.
427                         raise Http404
428                 
429                 if token_generator.check_token(user, email, token):
430                         user.email = email
431                         user.save()
432                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
433                         return HttpReponseRedirect(self.reverse('account', node=request.node))
434                 
435                 raise Http404
436         
437         class Meta:
438                 abstract = True