a98a8bcbff863c62ded409825458babf6737e38c
[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.core.urlresolvers import reverse
11 from django.db import models
12 from django.http import Http404, HttpResponseRedirect
13 from django.shortcuts import render_to_response, get_object_or_404
14 from django.template.defaultfilters import striptags
15 from django.utils.http import int_to_base36, base36_to_int
16 from django.utils.translation import ugettext_lazy, ugettext as _
17 from django.views.decorators.cache import never_cache
18 from django.views.decorators.csrf import csrf_protect
19 from philo.models import MultiView, Page
20 from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
21 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
22 import urlparse
23
24
25 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
26
27
28 def get_field_data(obj, fields):
29         if fields == None:
30                 fields = [field.name for field in obj._meta.fields if field.editable]
31         
32         return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
33
34
35 class LoginMultiView(MultiView):
36         """
37         Handles login, registration, and forgotten passwords. In other words, this
38         multiview provides exclusively view and methods related to usernames and
39         passwords.
40         """
41         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
42         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
43         password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
44         password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
45         password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
46         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
47         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
48         
49         @property
50         def urlpatterns(self):
51                 urlpatterns = patterns('',
52                         url(r'^login/$', self.login, name='login'),
53                         url(r'^logout/$', self.logout, name='logout'),
54                         
55                         url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
56                         url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.password_reset_confirm, name='password_reset_confirm'),
57                         
58                         url(r'^register/$', csrf_protect(self.register), name='register'),
59                         url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$', self.register_confirm, name='register_confirm')
60                 )
61                 
62                 if self.password_change_page:
63                         urlpatterns += patterns('',
64                                 url(r'^password/change/$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
65                         )
66                 
67                 return urlpatterns
68         
69         def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
70                 current_site = Site.objects.get_current()
71                 token = token_generator.make_token(user, *(token_args or []))
72                 kwargs = {
73                         'uidb36': int_to_base36(user.id),
74                         'token': token
75                 }
76                 kwargs.update(reverse_kwargs or {})
77                 return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
78                 
79         def get_context(self):
80                 """Hook for providing instance-specific context - such as the value of a Field - to all views."""
81                 return {}
82         
83         def display_login_page(self, request, message, extra_context=None):
84                 request.session.set_test_cookie()
85                 
86                 referrer = request.META.get('HTTP_REFERER', None)
87                 
88                 if referrer is not None:
89                         referrer = urlparse.urlparse(referrer)
90                         host = referrer[1]
91                         if host != request.get_host():
92                                 referrer = None
93                         else:
94                                 redirect = '%s?%s' % (referrer[2], referrer[4])
95                 
96                 if referrer is None:
97                         redirect = request.node.get_absolute_url()
98                 
99                 path = request.get_full_path()
100                 if redirect != path:
101                         if redirect is None:
102                                 redirect = '/'.join(path.split('/')[:-2])
103                         request.session['redirect'] = redirect
104                 
105                 if request.POST:
106                         form = LoginForm(request.POST)
107                 else:
108                         form = LoginForm()
109                 context = self.get_context()
110                 context.update(extra_context or {})
111                 context.update({
112                         'message': message,
113                         'form': form
114                 })
115                 return self.login_page.render_to_response(request, extra_context=context)
116         
117         def login(self, request, extra_context=None):
118                 """
119                 Displays the login form for the given HttpRequest.
120                 """
121                 if request.user.is_authenticated():
122                         return HttpResponseRedirect(request.node.get_absolute_url())
123                 
124                 context = self.get_context()
125                 context.update(extra_context or {})
126                 
127                 from django.contrib.auth.models import User
128                 
129                 # If this isn't already the login page, display it.
130                 if not request.POST.has_key(LOGIN_FORM_KEY):
131                         if request.POST:
132                                 message = _("Please log in again, because your session has expired.")
133                         else:
134                                 message = ""
135                         return self.display_login_page(request, message, context)
136
137                 # Check that the user accepts cookies.
138                 if not request.session.test_cookie_worked():
139                         message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
140                         return self.display_login_page(request, message, context)
141                 else:
142                         request.session.delete_test_cookie()
143                 
144                 # Check the password.
145                 username = request.POST.get('username', None)
146                 password = request.POST.get('password', None)
147                 user = authenticate(username=username, password=password)
148                 if user is None:
149                         message = ERROR_MESSAGE
150                         if username is not None and u'@' in username:
151                                 # Mistakenly entered e-mail address instead of username? Look it up.
152                                 try:
153                                         user = User.objects.get(email=username)
154                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
155                                         message = _("Usernames cannot contain the '@' character.")
156                                 else:
157                                         if user.check_password(password):
158                                                 message = _("Your e-mail address is not your username."
159                                                                         " Try '%s' instead.") % user.username
160                                         else:
161                                                 message = _("Usernames cannot contain the '@' character.")
162                         return self.display_login_page(request, message, context)
163
164                 # The user data is correct; log in the user in and continue.
165                 else:
166                         if user.is_active:
167                                 login(request, user)
168                                 try:
169                                         redirect = request.session.pop('redirect')
170                                 except KeyError:
171                                         redirect = request.node.get_absolute_url()
172                                 return HttpResponseRedirect(redirect)
173                         else:
174                                 return self.display_login_page(request, ERROR_MESSAGE, context)
175         login = never_cache(login)
176         
177         def logout(self, request):
178                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
179         
180         def login_required(self, view):
181                 def inner(request, *args, **kwargs):
182                         if not request.user.is_authenticated():
183                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
184                         return view(request, *args, **kwargs)
185                 
186                 return inner
187         
188         def send_confirmation_email(self, subject, email, page, extra_context):
189                 text_content = page.render_to_string(extra_context=extra_context)
190                 from_email = 'noreply@%s' % Site.objects.get_current().domain
191                 
192                 if page.template.mimetype == 'text/html':
193                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
194                         msg.attach_alternative(text_content, 'text/html')
195                         msg.send()
196                 else:
197                         send_mail(subject, text_content, from_email, [email])
198         
199         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
200                 if request.user.is_authenticated():
201                         return HttpResponseRedirect(request.node.get_absolute_url())
202                 
203                 if request.method == 'POST':
204                         form = PasswordResetForm(request.POST)
205                         if form.is_valid():
206                                 current_site = Site.objects.get_current()
207                                 for user in form.users_cache:
208                                         context = {
209                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
210                                                 'username': user.username
211                                         }
212                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
213                                         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)
214                                 return HttpResponseRedirect('')
215                 else:
216                         form = PasswordResetForm()
217                 
218                 context = self.get_context()
219                 context.update(extra_context or {})
220                 context.update({
221                         'form': form
222                 })
223                 return self.password_reset_page.render_to_response(request, extra_context=context)
224         
225         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
226                 """
227                 Checks that a given hash in a password reset link is valid. If so,
228                 displays the password set form.
229                 """
230                 assert uidb36 is not None and token is not None
231                 try:
232                         uid_int = base36_to_int(uidb36)
233                 except:
234                         raise Http404
235                 
236                 user = get_object_or_404(User, id=uid_int)
237                 
238                 if token_generator.check_token(user, token):
239                         if request.method == 'POST':
240                                 form = SetPasswordForm(user, request.POST)
241                                 
242                                 if form.is_valid():
243                                         form.save()
244                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
245                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
246                         else:
247                                 form = SetPasswordForm(user)
248                         
249                         context = self.get_context()
250                         context.update(extra_context or {})
251                         context.update({
252                                 'form': form
253                         })
254                         return self.password_set_page.render_to_response(request, extra_context=context)
255                 
256                 raise Http404
257         
258         def password_change(self, request, extra_context=None):
259                 if request.method == 'POST':
260                         form = PasswordChangeForm(request.user, request.POST)
261                         if form.is_valid():
262                                 form.save()
263                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
264                                 return HttpResponseRedirect('')
265                 else:
266                         form = PasswordChangeForm(request.user)
267                 
268                 context = self.get_context()
269                 context.update(extra_context or {})
270                 context.update({
271                         'form': form
272                 })
273                 return self.password_change_page.render_to_response(request, extra_context=context)
274         
275         def register(self, request, extra_context=None, token_generator=registration_token_generator):
276                 if request.user.is_authenticated():
277                         return HttpResponseRedirect(request.node.get_absolute_url())
278                 
279                 if request.method == 'POST':
280                         form = RegistrationForm(request.POST)
281                         if form.is_valid():
282                                 user = form.save()
283                                 context = {
284                                         'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
285                                 }
286                                 current_site = Site.objects.get_current()
287                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
288                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
289                                 return HttpResponseRedirect(request.node.get_absolute_url())
290                 else:
291                         form = RegistrationForm()
292                 
293                 context = self.get_context()
294                 context.update(extra_context or {})
295                 context.update({
296                         'form': form
297                 })
298                 return self.register_page.render_to_response(request, extra_context=context)
299         
300         def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
301                 """
302                 Checks that a given hash in a registration link is valid and activates
303                 the given account. If so, log them in and redirect to
304                 self.post_register_confirm_redirect.
305                 """
306                 assert uidb36 is not None and token is not None
307                 try:
308                         uid_int = base36_to_int(uidb36)
309                 except:
310                         raise Http404
311                 
312                 user = get_object_or_404(User, id=uid_int)
313                 if token_generator.check_token(user, token):
314                         user.is_active = True
315                         true_password = user.password
316                         temp_password = token_generator.make_token(user)
317                         try:
318                                 user.set_password(temp_password)
319                                 user.save()
320                                 authenticated_user = authenticate(username=user.username, password=temp_password)
321                                 login(request, authenticated_user)
322                         finally:
323                                 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
324                                 user.password = true_password
325                                 user.save()
326                         return self.post_register_confirm_redirect(request)
327                 
328                 raise Http404
329         
330         def post_register_confirm_redirect(self, request):
331                 return HttpResponseRedirect(request.node.get_absolute_url())
332         
333         class Meta:
334                 abstract = True
335
336
337 class AccountMultiView(LoginMultiView):
338         """
339         Subclasses may define an account_profile model, fields from the User model
340         to include in the account, and fields from the account profile to use in
341         the account.
342         """
343         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
344         email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
345         user_fields = ['first_name', 'last_name', 'email']
346         required_user_fields = user_fields
347         account_profile = None
348         account_profile_fields = None
349         
350         @property
351         def urlpatterns(self):
352                 urlpatterns = super(AccountMultiView, self).urlpatterns
353                 urlpatterns += patterns('',
354                         url(r'^account/$', self.login_required(self.account_view), name='account'),
355                         url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)/$', self.email_change_confirm, name='email_change_confirm')
356                 )
357                 return urlpatterns
358         
359         def get_account_forms(self):
360                 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
361                 
362                 if self.account_profile is None:
363                         profile_form = None
364                 else:
365                         profile_form = forms.models.modelform_factory(self.account_profile, fields=self.account_profile_fields or [field.name for field in self.account_profile._meta.fields if field.editable and field.name != 'user'])
366                 
367                 for field_name, field in user_form.base_fields.items():
368                         if field_name in self.required_user_fields:
369                                 field.required = True
370                 return user_form, profile_form
371         
372         def get_account_form_instances(self, user, data=None):
373                 form_instances = []
374                 user_form, profile_form = self.get_account_forms()
375                 if data is None:
376                         form_instances.append(user_form(instance=user))
377                         if profile_form:
378                                 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
379                 else:
380                         form_instances.append(user_form(data, instance=user))
381                         if profile_form:
382                                 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
383                 
384                 return form_instances
385         
386         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
387                 if request.method == 'POST':
388                         form_instances = self.get_account_form_instances(request.user, request.POST)
389                         current_email = request.user.email
390                         
391                         for form in form_instances:
392                                 if not form.is_valid():
393                                         break
394                         else:
395                                 # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
396                                 email = request.user.email
397                                 if current_email != email:
398                                         
399                                         request.user.email = current_email
400                                         
401                                         for form in form_instances:
402                                                 form.cleaned_data.pop('email', None)
403                                         
404                                         context = {
405                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
406                                         }
407                                         current_site = Site.objects.get_current()
408                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
409                                         messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
410                                         
411                                 for form in form_instances:
412                                         form.save()
413                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
414                                 return HttpResponseRedirect('')
415                 else:
416                         form_instances = self.get_account_form_instances(request.user)
417                 
418                 context = self.get_context()
419                 context.update(extra_context or {})
420                 context.update({
421                         'forms': form_instances
422                 })
423                 return self.manage_account_page.render_to_response(request, extra_context=context)
424         
425         def has_valid_account(self, user):
426                 user_form, profile_form = self.get_account_forms()
427                 forms = []
428                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
429                 
430                 if profile_form is not None:
431                         profile = self.account_profile._default_manager.get_or_create(user=user)[0]
432                         forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
433                 
434                 for form in forms:
435                         if not form.is_valid():
436                                 return False
437                 return True
438         
439         def account_required(self, view):
440                 def inner(request, *args, **kwargs):
441                         if not self.has_valid_account(request.user):
442                                 if not request.method == "POST":
443                                         messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
444                                 return self.account_view(request, *args, **kwargs)
445                         return view(request, *args, **kwargs)
446                 
447                 inner = self.login_required(inner)
448                 return inner
449         
450         def post_register_confirm_redirect(self, request):
451                 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
452                 return HttpResponseRedirect(self.reverse('account', node=request.node))
453         
454         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
455                 """
456                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
457                 """
458                 assert uidb36 is not None and token is not None and email is not None
459                 
460                 try:
461                         uid_int = base36_to_int(uidb36)
462                 except:
463                         raise Http404
464                 
465                 user = get_object_or_404(User, id=uid_int)
466                 
467                 email = '@'.join(email.rsplit('+', 1))
468                 
469                 if email == user.email:
470                         # Then short-circuit.
471                         raise Http404
472                 
473                 if token_generator.check_token(user, email, token):
474                         user.email = email
475                         user.save()
476                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
477                         return HttpReponseRedirect(self.reverse('account', node=request.node))
478                 
479                 raise Http404
480         
481         class Meta:
482                 abstract = True