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