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