Implemented one-time login on account confirm.
[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.sites.models import Site
8 from django.core.mail import send_mail
9 from django.core.urlresolvers import reverse
10 from django.db import models
11 from django.http import Http404, HttpResponseRedirect
12 from django.shortcuts import render_to_response, get_object_or_404
13 from django.utils.http import int_to_base36, base36_to_int
14 from django.utils.translation import ugettext_lazy, ugettext as _
15 from django.views.decorators.cache import never_cache
16 from django.views.decorators.csrf import csrf_protect
17 from philo.models import MultiView, Page
18 from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
19 from philo.contrib.waldo.tokens import default_token_generator
20
21
22 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
23
24
25 def get_field_data(obj, fields):
26         if fields == None:
27                 fields = [field.name for field in obj._meta.fields if field.editable]
28         
29         return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
30
31
32 class LoginMultiView(MultiView):
33         """
34         Handles login, registration, and forgotten passwords. In other words, this
35         multiview provides exclusively view and methods related to usernames and
36         passwords.
37         """
38         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
39         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
40         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
41         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
42         
43         @property
44         def urlpatterns(self):
45                 urlpatterns = patterns('',
46                         url(r'^login/$', self.login, name='login'),
47                         url(r'^logout/$', self.logout, name='logout')
48                 )
49                 urlpatterns += patterns('',
50                         url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
51                         url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
52                                 self.password_reset_confirm, name='password_reset_confirm')
53                 )
54                 urlpatterns += patterns('',
55                         url(r'^register/$', csrf_protect(self.register), name='register'),
56                         url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
57                                 self.register_confirm, name='register_confirm')
58                 )
59                 return urlpatterns
60         
61         def get_context(self, extra_dict=None):
62                 context = {}
63                 context.update(extra_dict or {})
64                 return context
65         
66         def display_login_page(self, request, message, node=None, extra_context=None):
67                 request.session.set_test_cookie()
68                 
69                 redirect = request.META.get('HTTP_REFERER', None)
70                 path = request.get_full_path()
71                 if redirect != path:
72                         if redirect is None:
73                                 redirect = '/'.join(path.split('/')[:-2])
74                         request.session['redirect'] = redirect
75                 
76                 if request.POST:
77                         form = LoginForm(request.POST)
78                 else:
79                         form = LoginForm()
80                 context = self.get_context({
81                         'message': message,
82                         'form': form
83                 })
84                 context.update(extra_context or {})
85                 return self.login_page.render_to_response(node, request, extra_context=context)
86         
87         def login(self, request, node=None, extra_context=None):
88                 """
89                 Displays the login form for the given HttpRequest.
90                 """
91                 context = self.get_context(extra_context)
92                 
93                 from django.contrib.auth.models import User
94                 
95                 # If this isn't already the login page, display it.
96                 if not request.POST.has_key(LOGIN_FORM_KEY):
97                         if request.POST:
98                                 message = _("Please log in again, because your session has expired.")
99                         else:
100                                 message = ""
101                         return self.display_login_page(request, message, node, context)
102
103                 # Check that the user accepts cookies.
104                 if not request.session.test_cookie_worked():
105                         message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
106                         return self.display_login_page(request, message, node, context)
107                 else:
108                         request.session.delete_test_cookie()
109                 
110                 # Check the password.
111                 username = request.POST.get('username', None)
112                 password = request.POST.get('password', None)
113                 user = authenticate(username=username, password=password)
114                 if user is None:
115                         message = ERROR_MESSAGE
116                         if username is not None and u'@' in username:
117                                 # Mistakenly entered e-mail address instead of username? Look it up.
118                                 try:
119                                         user = User.objects.get(email=username)
120                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
121                                         message = _("Usernames cannot contain the '@' character.")
122                                 else:
123                                         if user.check_password(password):
124                                                 message = _("Your e-mail address is not your username."
125                                                                         " Try '%s' instead.") % user.username
126                                         else:
127                                                 message = _("Usernames cannot contain the '@' character.")
128                         return self.display_login_page(request, message, node, context)
129
130                 # The user data is correct; log in the user in and continue.
131                 else:
132                         if user.is_active:
133                                 login(request, user)
134                                 redirect = request.session.pop('redirect')
135                                 return HttpResponseRedirect(redirect)
136                         else:
137                                 return self.display_login_page(request, ERROR_MESSAGE, node, context)
138         login = never_cache(login)
139         
140         def logout(self, request):
141                 return auth_views.logout(request, request.META['HTTP_REFERER'])
142         
143         def login_required(self, view):
144                 def inner(request, node=None, *args, **kwargs):
145                         if not request.user.is_authenticated():
146                                 login_url = reverse('login', urlconf=self).strip('/')
147                                 return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
148                         return view(request, node=node, *args, **kwargs)
149                 
150                 return inner
151         
152         def send_confirmation_email(self, subject, email, page, extra_context):
153                 message = page.render_to_string(extra_context=extra_context)
154                 from_email = 'noreply@%s' % Site.objects.get_current().domain
155                 send_mail(subject, message, from_email, [email])
156         
157         def password_reset(self, request, node=None, extra_context=None):
158                 pass
159         
160         def password_reset_confirm(self, request, node=None, extra_context=None):
161                 pass
162         
163         def register(self, request, node=None, extra_context=None, token_generator=default_token_generator):
164                 if request.user.is_authenticated():
165                         return HttpResponseRedirect(node.get_absolute_url())
166                 
167                 if request.method == 'POST':
168                         form = RegistrationForm(request.POST)
169                         if form.is_valid():
170                                 user = form.save()
171                                 current_site = Site.objects.get_current()
172                                 token = default_token_generator.make_token(user)
173                                 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('/'))
174                                 context = {
175                                         'link': link
176                                 }
177                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
178                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email)
179                                 return HttpResponseRedirect('')
180                 else:
181                         form = RegistrationForm()
182                 
183                 context = self.get_context({'form': form})
184                 context.update(extra_context or {})
185                 return self.register_page.render_to_response(node, request, extra_context=context)
186         
187         def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None):
188                 """
189                 Checks that a given hash in a registration link is valid and activates
190                 the given account. If so, log them in and redirect to
191                 self.post_register_confirm_redirect.
192                 """
193                 assert uidb36 is not None and token is not None
194                 try:
195                         uid_int = base36_to_int(uidb36)
196                 except:
197                         raise Http404
198                 
199                 user = get_object_or_404(User, id=uid_int)
200                 if default_token_generator.check_token(user, token):
201                         user.is_active = True
202                         true_password = user.password
203                         try:
204                                 user.set_password('temp_password')
205                                 user.save()
206                                 authenticated_user = authenticate(username=user.username, password='temp_password')
207                                 login(request, authenticated_user)
208                         finally:
209                                 # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
210                                 user.password = true_password
211                                 user.save()
212                         return self.post_register_confirm_redirect(request, node)
213                 
214                 raise Http404
215         
216         def post_register_confirm_redirect(self, request, node):
217                 return HttpResponseRedirect(node.get_absolute_url())
218         
219         class Meta:
220                 abstract = True
221
222
223 class AccountMultiView(LoginMultiView):
224         """
225         Subclasses may define an account_profile model, fields from the User model
226         to include in the account, and fields from the account profile to use in
227         the account.
228         """
229         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_page')
230         user_fields = ['first_name', 'last_name', 'email']
231         required_user_fields = user_fields
232         account_profile = None
233         account_profile_fields = None
234         
235         @property
236         def urlpatterns(self):
237                 urlpatterns = super(AccountMultiView, self).urlpatterns
238                 urlpatterns += patterns('',
239                         url(r'^account/$', self.login_required(self.account_view), name='account')
240                 )
241                 return urlpatterns
242         
243         def get_account_forms(self):
244                 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
245                 
246                 if self.account_profile is None:
247                         profile_form = None
248                 else:
249                         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'])
250                 
251                 for field_name, field in user_form.base_fields.items():
252                         if field_name in self.required_user_fields:
253                                 field.required = True
254                 return user_form, profile_form
255         
256         def get_account_form_instances(self, user, data=None):
257                 form_instances = []
258                 user_form, profile_form = self.get_account_forms()
259                 if data is None:
260                         form_instances.append(user_form(instance=user))
261                         if profile_form:
262                                 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
263                 else:
264                         form_instances.append(user_form(data, instance=user))
265                         if profile_form:
266                                 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
267                 
268                 return form_instances
269         
270         def account_view(self, request, node=None, extra_context=None):
271                 if request.method == 'POST':
272                         form_instances = self.get_account_form_instances(request.user, request.POST)
273                         
274                         for form in form_instances:
275                                 if not form.is_valid():
276                                         break
277                         else:
278                                 for form in form_instances:
279                                         form.save()
280                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
281                                 return HttpResponseRedirect('')
282                 else:
283                         form_instances = self.get_account_form_instances(request.user)
284                 
285                 context = self.get_context({
286                         'forms': form_instances
287                 })
288                 context.update(extra_context or {})
289                 return self.manage_account_page.render_to_response(node, request, extra_context=context)
290         
291         def has_valid_account(self, user):
292                 user_form, profile_form = self.get_account_forms()
293                 forms = []
294                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
295                 
296                 if profile_form is not None:
297                         profile = self.account_profile._default_manager.get_or_create(user=user)[0]
298                         forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
299                 
300                 for form in forms:
301                         if not form.is_valid():
302                                 return False
303                 return True
304         
305         def account_required(self, view):
306                 def inner(request, *args, **kwargs):
307                         if not self.has_valid_account(request.user):
308                                 messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.")
309                                 return self.account_view(request, *args, **kwargs)
310                         return view(request, *args, **kwargs)
311                 
312                 inner = self.login_required(inner)
313                 return inner
314         
315         def post_register_confirm_redirect(self, request, node):
316                 messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.')
317                 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
318         
319         class Meta:
320                 abstract = True