WIP: Implementation of pended user creation using tokens. Needs some testing.
authorStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 10 Aug 2010 22:29:41 +0000 (18:29 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Mon, 23 Aug 2010 13:43:55 +0000 (09:43 -0400)
contrib/waldo/forms.py [new file with mode: 0644]
contrib/waldo/models.py
contrib/waldo/tokens.py [new file with mode: 0644]

diff --git a/contrib/waldo/forms.py b/contrib/waldo/forms.py
new file mode 100644 (file)
index 0000000..50e1fa6
--- /dev/null
@@ -0,0 +1,49 @@
+from datetime import date
+from django import forms
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+
+LOGIN_FORM_KEY = 'this_is_the_login_form'
+LoginForm = type('LoginForm', (AuthenticationForm,), {
+       LOGIN_FORM_KEY: forms.BooleanField(widget=forms.HiddenInput, initial=True)
+})
+
+
+class EmailInput(forms.TextInput):
+       input_type = 'email'
+
+
+class RegistrationForm(UserCreationForm):
+       email = forms.EmailField(widget=EmailInput)
+       
+       def clean_username(self):
+               username = self.cleaned_data['username']
+               
+               # Trivial case: if the username doesn't exist, go for it!
+               try:
+                       user = User.objects.get(username=username)
+               except User.DoesNotExist:
+                       return username
+               
+               if not user.is_active and (date.today() - user.date_joined.date()).days > REGISTRATION_TIMEOUT_DAYS and user.last_login == user.date_joined:
+                       # Then this is a user who has not confirmed their registration and whose time is up. Delete the old user and return the username.
+                       user.delete()
+                       return username
+               
+               raise ValidationError(_("A user with that username already exists."))
+       
+       def clean_email(self):
+               if User.objects.filter(email__iexact=self.cleaned_data['email']):
+                       raise ValidationError(_('This email is already in use. Please supply a different email address'))
+               return self.cleaned_data['email']
+       
+       def save(self):
+               username = self.cleaned_data['username']
+               email = self.cleaned_data['email']
+               password = self.cleaned_data['password1']
+               new_user = User.objects.create_user(username, email, password)
+               new_user.is_active = False
+               new_user.save()
+               return new_user
\ No newline at end of file
index d0669ff..1c29594 100644 (file)
@@ -2,22 +2,24 @@ from django import forms
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, views as auth_views
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, views as auth_views
-from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
 from django.db import models
 from django.http import Http404, HttpResponseRedirect
 from django.shortcuts import render_to_response
 from django.db import models
 from django.http import Http404, HttpResponseRedirect
 from django.shortcuts import render_to_response
+from django.utils.http import int_to_base36
 from django.utils.translation import ugettext_lazy, ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 from philo.models import MultiView, Page
 from django.utils.translation import ugettext_lazy, ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 from philo.models import MultiView, Page
+from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
+from philo.contrib.waldo.tokens import default_token_generator
 
 
 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
 
 
 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
-LOGIN_FORM_KEY = 'this_is_the_login_form'
-LoginForm = type('LoginForm', (AuthenticationForm,), {
-       LOGIN_FORM_KEY: forms.BooleanField(widget=forms.HiddenInput, initial=True)
-})
 
 
 def get_field_data(obj, fields):
 
 
 def get_field_data(obj, fields):
@@ -28,7 +30,15 @@ def get_field_data(obj, fields):
 
 
 class LoginMultiView(MultiView):
 
 
 class LoginMultiView(MultiView):
-       login_page = models.ForeignKey(Page, related_name='login_related')
+       """
+       Handles login, registration, and forgotten passwords. In other words, this
+       multiview provides exclusively view and methods related to usernames and
+       passwords.
+       """
+       login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
+       password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
+       register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
+       register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
        
        @property
        def urlpatterns(self):
        
        @property
        def urlpatterns(self):
@@ -36,6 +46,16 @@ class LoginMultiView(MultiView):
                        url(r'^login/$', self.login, name='login'),
                        url(r'^logout/$', self.logout, name='logout')
                )
                        url(r'^login/$', self.login, name='login'),
                        url(r'^logout/$', self.logout, name='logout')
                )
+               urlpatterns += patterns('',
+                       url(r'^password/reset/$', self.password_reset, name='password_reset'),
+                       url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
+                               self.password_reset_confirm, name='password_reset_confirm')
+               )
+               urlpatterns += patterns('',
+                       url(r'^register/$', self.register, name='register'),
+                       url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
+                               self.register_confirm, name='register_confirm')
+               )
                return urlpatterns
        
        def get_context(self, extra_dict=None):
                return urlpatterns
        
        def get_context(self, extra_dict=None):
@@ -123,13 +143,43 @@ class LoginMultiView(MultiView):
        def login_required(self, view):
                def inner(request, node=None, *args, **kwargs):
                        if not request.user.is_authenticated():
        def login_required(self, view):
                def inner(request, node=None, *args, **kwargs):
                        if not request.user.is_authenticated():
-                               root_url = node.get_path(Site.objects.get_current().root_node).strip('/')
                                login_url = reverse('login', urlconf=self).strip('/')
                                login_url = reverse('login', urlconf=self).strip('/')
-                               return HttpResponseRedirect('/%s/%s/' % (root_url, login_url))
+                               return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
                        return view(request, node=node, *args, **kwargs)
                
                return inner
        
                        return view(request, node=node, *args, **kwargs)
                
                return inner
        
+       def send_confirmation_email(self, subject, email, page, extra_context):
+               message = page.render_to_string(extra_context=extra_context)
+               from_email = 'noreply@%s' % Site.objects.get_current().domain
+               send_mail(subject, message, from_email, [email])
+       
+       @csrf_protect
+       def password_reset(self, request, node=None, extra_context=None):
+               pass
+       
+       @csrf_protect
+       def register(self, request, node=None, extra_context=None, token_generator=default_token_generator):
+               if request.method == POST:
+                       form = RegistrationForm(request.POST)
+                       if form.is_valid():
+                               user = form.save()
+                               current_site = Site.objects.get_current()
+                               token = default_token_generator.make_token(user)
+                               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('/'))
+                               context = {
+                                       'link': link
+                               }
+                               self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
+                               messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email)
+                               return HttpResponseRedirect('')
+               else:
+                       form = RegistrationForm()
+               
+               context = self.get_context({'form': form})
+               context.update(extra_context or {})
+               return self.register_page.render_to_response(request, node, context)
+       
        class Meta:
                abstract = True
 
        class Meta:
                abstract = True
 
@@ -140,7 +190,7 @@ class AccountMultiView(LoginMultiView):
        to include in the account, and fields from the account profile to use in
        the account.
        """
        to include in the account, and fields from the account profile to use in
        the account.
        """
-       manage_account_page = models.ForeignKey(Page, related_name='classifieds_manage_account_page')
+       manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_page')
        user_fields = ['first_name', 'last_name', 'email']
        required_user_fields = user_fields
        account_profile = None
        user_fields = ['first_name', 'last_name', 'email']
        required_user_fields = user_fields
        account_profile = None
diff --git a/contrib/waldo/tokens.py b/contrib/waldo/tokens.py
new file mode 100644 (file)
index 0000000..aff91ff
--- /dev/null
@@ -0,0 +1,66 @@
+"""
+Based on django.contrib.auth.tokens
+"""
+
+
+from datetime import date
+from django.conf import settings
+from django.utils.http import int_to_base36, base36_to_int
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+
+
+REGISTRATION_TIMEOUT_DAYS = getattr(settings, 'WALDO_REGISTRATION_TIMEOUT_DAYS', 1)
+
+
+class RegistrationTokenGenerator(PasswordResetTokenGenerator):
+       """
+       Strategy object used to generate and check tokens for the user registration mechanism.
+       """
+       def make_token(self, user):
+               """
+               Returns a token that can be used once to activate a user's account.
+               """
+               if user.is_active:
+                       return False
+               return self._make_token_with_timestamp(user, self._num_days(self._today()))
+       
+       def check_token(self, user, token):
+               """
+               Check that a registration token is correct for a given user.
+               """
+               # If the user is active, the hash can't be valid.
+               if user.is_active:
+                       return False
+               
+               # Parse the token
+               try:
+                       ts_b36, hash = token.split('-')
+               except ValueError:
+                       return False
+               
+               try:
+                       ts = base36_to_int(ts_b36)
+               except ValueError:
+                       return False
+               
+               # Check that the timestamp and uid have not been tampered with.
+               if self._make_token_with_timestamp(user, ts) != token:
+                       return False
+               
+               # Check that the timestamp is within limit
+               if (self._num_days(self._today()) - ts) > REGISTRATION_TIMEOUT_DAYS:
+                       return False
+               
+               return True
+       
+       def _make_token_with_timestamp(self, user, timestamp):
+               ts_b36 = int_to_base36(timestamp)
+               
+               # By hashing on the internal state of the user and using state that is
+               # sure to change, we produce a hash that will be invalid as soon as it
+               # is used.
+               from django.utils.hashcompat import sha_constructor
+               hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) + unicode(user.is_active) + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + unicode(timestamp)).hexdigest()[::2]
+               return '%s-%s' % (ts_b36, hash)
+
+default_token_generator = RegistrationTokenGenerator()
\ No newline at end of file