Commits

Cole Krumbholz  committed 0203808

added email-only registration backend, plays nice with existing backends, even in the same user database

  • Participants
  • Parent commits 58eef83

Comments (0)

Files changed (5)

File registration/__init__.py

             if VERSION[4] != 0:
                 version = '%s %s' % (version, VERSION[4])
     return version
+
+
+"""
+EmailAuthBackend 
+
+@author: chris http://djangosnippets.org/snippets/1845/
+updated to 1.3 by Lukasz Kidzinski
+"""
+
+from django.contrib.auth.models import User
+from django.core.validators import email_re
+
+class EmailAuthBackend:
+    """
+    Authenticate with e-mail.
+
+    Use the  e-mail, and password
+    
+    Should work with django 1.3
+    """
+
+    supports_object_permissions = False
+    supports_anonymous_user = False
+    supports_inactive_user = False
+
+    def authenticate(self, username=None, password=None):
+        if email_re.search(username):
+            try:
+                user = User.objects.get(email=username)
+            except User.DoesNotExist:
+                return None
+        else:
+            #We have a non-email address username we should try username
+            try:
+                user = User.objects.get(username=username)
+            except User.DoesNotExist:
+                return None
+        if user.check_password(password):
+            return user
+        return None
+
+    def get_user(self, user_id):
+        try:
+            return User.objects.get(pk=user_id)
+        except User.DoesNotExist:
+            return None

File registration/backends/simpleemail/__init__.py

+from django.conf import settings
+from django.contrib.sites.models import RequestSite
+from django.contrib.sites.models import Site
+from django.contrib.auth import authenticate, login
+
+from registration import signals
+from registration.forms import RegistrationFormNoUserName
+from registration.models import RegistrationProfile
+
+
+class SimpleEmailBackend(object):
+    """
+    A registration backend which follows a simple workflow:
+
+    1. User signs up with only an email address and first/last
+        name, and is automatically logged into an active account.
+
+    2. Email is sent to user with an activation link for
+        email validation.
+
+    Using this backend requires that
+
+    * ``registration`` be listed in the ``INSTALLED_APPS`` setting
+      (since this backend makes use of models defined in this
+      application).
+
+    * The setting ``ACCOUNT_ACTIVATION_DAYS`` be supplied, specifying
+      (as an integer) the number of days from registration during
+      which a user may activate their account (after that period
+      expires, activation will be disallowed).
+
+    * The creation of the templates
+      ``registration/activation_email_subject.txt`` and
+      ``registration/activation_email.txt``, which will be used for
+      the activation email. See the notes for this backends
+      ``register`` method for details regarding these templates.
+
+    * The EmailAuthBackend should be installed. You can install it 
+      alongside the default auth backend, allowing both traditional 
+      and email-only authentication:
+
+        AUTHENTICATION_BACKENDS = (
+            'registration.EmailAuthBackend',
+            'django.contrib.auth.backends.ModelBackend',
+        )
+
+    Additionally, registration can be temporarily closed by adding the
+    setting ``REGISTRATION_OPEN`` and setting it to
+    ``False``. Omitting this setting, or setting it to ``True``, will
+    be interpreted as meaning that registration is currently open and
+    permitted.
+
+    Internally, this is accomplished via storing an activation key in
+    an instance of ``registration.models.RegistrationProfile``. See
+    that model and its custom manager for full documentation of its
+    fields and supported operations.
+    
+    RegistrationProfile can be checked later to determine if the 
+    email was validated. The following example shows how:
+    
+    is_validated = request.user.registration_profile.is_validated()
+    
+    """
+    def register(self, request, **kwargs):
+        """
+        Given a username, email address and password, register a new
+        user account, which will initially be inactive.
+
+        Along with the new ``User`` object, a new
+        ``registration.models.RegistrationProfile`` will be created,
+        tied to that ``User``, containing the activation key which
+        will be used for this account.
+
+        An email will be sent to the supplied email address; this
+        email should contain an activation link. The email will be
+        rendered using two templates. See the documentation for
+        ``RegistrationProfile.send_activation_email()`` for
+        information about these templates and the contexts provided to
+        them.
+
+        After the ``User`` and ``RegistrationProfile`` are created and
+        the activation email is sent, the signal
+        ``registration.signals.user_registered`` will be sent, with
+        the new ``User`` as the keyword argument ``user`` and the
+        class of this backend as the sender.
+
+        """
+        username, email, password = kwargs['username'], kwargs['email'], kwargs['password1']
+        first_name, last_name = kwargs['first_name'], kwargs['last_name']
+
+        if Site._meta.installed:
+            site = Site.objects.get_current()
+        else:
+            site = RequestSite(request)
+        new_user = RegistrationProfile.objects.create_user(username, email, password, site,
+                                                           first_name=first_name, last_name=last_name)
+
+        new_user = authenticate(username=username, password=password)
+        login(request, new_user)
+
+        signals.user_registered.send(sender=self.__class__,
+                                     user=new_user,
+                                     request=request)
+        return new_user
+
+    def activate(self, request, activation_key):
+        """
+        Given an an activation key, look up and activate the user
+        account corresponding to that key (if possible).
+
+        After successful activation, the signal
+        ``registration.signals.user_activated`` will be sent, with the
+        newly activated ``User`` as the keyword argument ``user`` and
+        the class of this backend as the sender.
+        
+        """
+        activated = RegistrationProfile.objects.activate_user(activation_key)
+        if activated:
+            signals.user_activated.send(sender=self.__class__,
+                                        user=activated,
+                                        request=request)
+        return activated
+
+    def registration_allowed(self, request):
+        """
+        Indicate whether account registration is currently permitted,
+        based on the value of the setting ``REGISTRATION_OPEN``. This
+        is determined as follows:
+
+        * If ``REGISTRATION_OPEN`` is not specified in settings, or is
+          set to ``True``, registration is permitted.
+
+        * If ``REGISTRATION_OPEN`` is both specified and set to
+          ``False``, registration is not permitted.
+        
+        """
+        return getattr(settings, 'REGISTRATION_OPEN', True)
+
+    def get_form_class(self, request):
+        """
+        Return the default form class used for user registration.
+        
+        """
+        return RegistrationFormNoUserName
+
+    def post_registration_redirect(self, request, user):
+        """
+        Return the name of the URL to redirect to after successful
+        user registration.
+        
+        """
+        return (getattr(settings, 'LOGIN_REDIRECT_URL', 'registration_complete'), (), {})
+
+    def post_activation_redirect(self, request, user):
+        """
+        Return the name of the URL to redirect to after successful
+        account activation.
+        
+        """
+        return ('registration_activation_complete', (), {})
+

File registration/backends/simpleemail/urls.py

+"""
+URLconf for registration and activation, using django-registration's
+default backend.
+
+If the default behavior of these views is acceptable to you, simply
+use a line like this in your root URLconf to set up the default URLs
+for registration::
+
+    (r'^accounts/', include('registration.backends.default.urls')),
+
+This will also automatically set up the views in
+``django.contrib.auth`` at sensible default locations.
+
+If you'd like to customize the behavior (e.g., by passing extra
+arguments to the various views) or split up the URLs, feel free to set
+up your own URL patterns for these views instead.
+
+"""
+
+
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+from django.contrib.auth import views as auth_views
+
+from registration.views import activate
+from registration.views import register
+from registration.forms import EmailOnlyAuthenticationForm
+
+urlpatterns = patterns('',
+                       url(r'^activate/complete/$',
+                           direct_to_template,
+                           {'template': 'registration/activation_complete.html'},
+                           name='registration_activation_complete'),
+                       # Activation keys get matched by \w+ instead of the more specific
+                       # [a-fA-F0-9]{40} because a bad activation key should still get to the view;
+                       # that way it can return a sensible "invalid key" message instead of a
+                       # confusing 404.
+                       url(r'^activate/(?P<activation_key>\w+)/$',
+                           activate,
+                           {'backend': 'registration.backends.simpleemail.SimpleEmailBackend'},
+                           name='registration_activate'),
+                       url(r'^register/$',
+                           register,
+                           {'backend': 'registration.backends.simpleemail.SimpleEmailBackend'},
+                           name='registration_register'),
+                       url(r'^register/complete/$',
+                           direct_to_template,
+                           {'template': 'registration/registration_complete.html'},
+                           name='registration_complete'),
+                       url(r'^register/closed/$',
+                           direct_to_template,
+                           {'template': 'registration/registration_closed.html'},
+                           name='registration_disallowed'),
+                       # Override the default login behavior to allow for long email usernames
+                       url(r'^login/$',
+                           auth_views.login,
+                           {'template_name': 'registration/login.html',
+                            'authentication_form': EmailOnlyAuthenticationForm, },
+                           name='auth_login'),
+                       (r'', include('registration.auth_urls')),
+                       )

File registration/forms.py

 from django.contrib.auth.models import User
 from django import forms
 from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.forms import AuthenticationForm
 
 
 # I put this on all required fields, because it's easier to pick up
         return self.cleaned_data['email']
 
 
+class RegistrationFormNoUserName(RegistrationFormUniqueEmail):
+    """
+    Based on http://djangosnippets.org/snippets/686/ with modifications by Cole Krumbholz
+    
+    A registration form that only requires the user to enter their e-mail 
+    address and password. The username is automatically generated
+    This class requires django-registration to extend the 
+    RegistrationFormUniqueEmail.
+    
+    """ 
+    username = forms.CharField(widget=forms.HiddenInput, max_length=75, required=False)
+
+    email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, maxlength=75)),
+                             label=_("E-mail / Username"))
+
+    first_name = forms.CharField(max_length=30)
+
+    last_name  = forms.CharField(max_length=30)
+
+    def clean_username(self):
+        "This function is required to overwrite an inherited username clean"
+        return self.cleaned_data['username']
+
+    def clean(self):
+        if not self.errors:
+            # originally this snipped shortened the username using the following encoding of the email:
+            #
+            # self.cleaned_data['username'] = '%s%s' % (self.cleaned_data['email'].split('@',1)[0], User.objects.count())
+            #
+            # In my limited testing I have not found a problem with usernames > 30 characters long
+            # so I simply use the email for the username:
+            
+            self.cleaned_data['username'] = self.cleaned_data['email']
+            
+        super(RegistrationFormNoUserName, self).clean()
+        return self.cleaned_data
+
+    def __init__(self, *args, **kwargs):
+        # reorder the fields
+        # see discussion at http://stackoverflow.com/questions/913589/django-forms-inheritance-and-order-of-form-fields
+        
+        super(RegistrationFormNoUserName, self).__init__(*args, **kwargs)
+        self.fields.keyOrder = ['email', 'first_name', 'last_name', 'password1', 'password2']
+
+
 class RegistrationFormNoFreeEmail(RegistrationForm):
     """
     Subclass of ``RegistrationForm`` which disallows registration with
         if email_domain in self.bad_domains:
             raise forms.ValidationError(_("Registration using free email addresses is prohibited. Please supply a different email address."))
         return self.cleaned_data['email']
+
+
+class EmailOnlyAuthenticationForm(AuthenticationForm):
+    """
+    Subclass of django.contrib.auth.AuthenticationForm that accepts
+    emails for usernames
+    """
+    username = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, maxlength=75)), 
+                                label=_("E-mail / Username"))

File registration/models.py

     
     def create_inactive_user(self, username, email, password,
                              site, send_email=True):
+        self.create_user(username, email, password, site=site, active=False)
+
+    def create_user(self, username, email, password, site, send_email=True, 
+                    first_name='', last_name='', active=True):
         """
         Create a new, inactive ``User``, generate a
         ``RegistrationProfile`` and email its activation key to the
         
         """
         new_user = User.objects.create_user(username, email, password)
-        new_user.is_active = False
+        new_user.first_name = first_name
+        new_user.last_name = last_name
+        new_user.is_active = active
         new_user.save()
-
+        
         registration_profile = self.create_profile(new_user)
 
         if send_email:
             registration_profile.send_activation_email(site)
 
         return new_user
-    create_inactive_user = transaction.commit_on_success(create_inactive_user)
+    create_user = transaction.commit_on_success(create_user)
 
     def create_profile(self, user):
         """
     """
     ACTIVATED = u"ALREADY_ACTIVATED"
     
-    user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
+    user = models.OneToOneField(User, verbose_name=_('user'), related_name='registration_profile')
     activation_key = models.CharField(_('activation key'), max_length=40)
     
     objects = RegistrationManager()
     def __unicode__(self):
         return u"Registration information for %s" % self.user
     
+    def is_validated(self):
+        """
+        In the case where users are set active upon registration, 
+        the activation key can still be used to validate the user's
+        email address. In this case, The is_validated method can be 
+        used to check if the email supplied is valid.
+        """
+        return self.activation_key == self.ACTIVATED
+    
     def activation_key_expired(self):
         """
         Determine whether this ``RegistrationProfile``'s activation
                                    ctx_dict)
         
         self.user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
-    
+