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