first draft of a registration form + email
[myslice.git] / portal / models.py
index 3c950a6..da0e086 100644 (file)
 # this program; see the file COPYING.  If not, write to the Free Software
 # Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
-from django.db import models
-from .validators import validate_email
+import datetime
+import hashlib
+import random
+import re
 
+from django.conf              import settings
+from django.core.mail         import send_mail
+from django.db                import models
+from django.db                import transaction
+from django.utils.translation import ugettext_lazy as _
+from django.template.loader   import render_to_string
+
+#from django.core.validators import validate_email
+
+try:
+    from django.contrib.auth import get_user_model
+    User = get_user_model()
+except ImportError:
+    from django.contrib.auth.models import User
+
+try:
+    from django.utils.timezone import now as datetime_now
+except ImportError:
+    datetime_now = datetime.datetime.now
+
+SHA1_RE = re.compile('^[a-f0-9]{40}$')
 
 # Create your models here.
 
@@ -30,15 +53,257 @@ class Institution(models.Model):
     name = models.TextField()
     # list of associated email domains 
 
+# code borrowed from django-registration
+# https://bitbucket.org/ubernostrum/django-registration/
+
+class RegistrationManager(models.Manager):
+    """
+    Custom manager for the ``RegistrationProfile`` model.
+    
+    The methods defined here provide shortcuts for account creation
+    and activation (including generation and emailing of activation
+    keys), and for cleaning out expired inactive accounts.
+    
+    """
+    def activate_user(self, activation_key):
+        """
+        Validate an activation key and activate the corresponding
+        ``User`` if valid.
+        
+        If the key is valid and has not expired, return the ``User``
+        after activating.
+        
+        If the key is not valid or has expired, return ``False``.
+        
+        If the key is valid but the ``User`` is already active,
+        return ``False``.
+        
+        To prevent reactivation of an account which has been
+        deactivated by site administrators, the activation key is
+        reset to the string constant ``RegistrationProfile.ACTIVATED``
+        after successful activation.
+
+        """
+        # Make sure the key we're trying conforms to the pattern of a
+        # SHA1 hash; if it doesn't, no point trying to look it up in
+        # the database.
+        if SHA1_RE.search(activation_key):
+            try:
+                profile = self.get(activation_key=activation_key)
+            except self.model.DoesNotExist:
+                return False
+            if not profile.activation_key_expired():
+                user = profile.user
+                user.is_active = True
+                user.save()
+                profile.activation_key = self.model.ACTIVATED
+                profile.save()
+                return user
+        return False
+    
+    def create_user(self, first_name, last_name, email, password):
+        pending_user = self.create(first_name=first_name, last_name=last_name, email=email, password=password)
+        return pending_user
+
+    def create_inactive_user(self, first_name, last_name, email, password, site,
+                             send_email=True):
+        """
+        Create a new, inactive ``User``, generate a
+        ``RegistrationProfile`` and email its activation key to the
+        ``User``, returning the new ``User``.
+
+        By default, an activation email will be sent to the new
+        user. To disable this, pass ``send_email=False``.
+        
+        """
+        salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
+        if isinstance(email, unicode):
+            email = email.encode('utf-8')
+        activation_key = hashlib.sha1(salt+email).hexdigest()
+
+        new_user = PendingUser.objects.create_user(first_name, last_name, email, password)
+        new_user.is_active = False
+        new_user.activation_key=activation_key
+        new_user.save()
+
+        # We might not need this
+        #registration_profile = self.create_profile(new_user)
+
+        if send_email:
+            new_user.send_activation_email(site)
+            #registration_profile.send_activation_email(site)
+
+        return new_user
+    create_inactive_user = transaction.commit_on_success(create_inactive_user)
+
+    def create_profile(self, user):
+        """
+        Create a ``RegistrationProfile`` for a given
+        ``User``, and return the ``RegistrationProfile``.
+        
+        The activation key for the ``RegistrationProfile`` will be a
+        SHA1 hash, generated from a combination of the ``User``'s
+        username and a random salt.
+        
+        """
+        salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
+        username = user.username
+        if isinstance(username, unicode):
+            username = username.encode('utf-8')
+        activation_key = hashlib.sha1(salt+username).hexdigest()
+        return self.create(user=user,
+                           activation_key=activation_key)
+        
+    def delete_expired_users(self):
+        """
+        Remove expired instances of ``RegistrationProfile`` and their
+        associated ``User``s.
+        
+        Accounts to be deleted are identified by searching for
+        instances of ``RegistrationProfile`` with expired activation
+        keys, and then checking to see if their associated ``User``
+        instances have the field ``is_active`` set to ``False``; any
+        ``User`` who is both inactive and has an expired activation
+        key will be deleted.
+        
+        It is recommended that this method be executed regularly as
+        part of your routine site maintenance; this application
+        provides a custom management command which will call this
+        method, accessible as ``manage.py cleanupregistration``.
+        
+        Regularly clearing out accounts which have never been
+        activated serves two useful purposes:
+        
+        1. It alleviates the ocasional need to reset a
+           ``RegistrationProfile`` and/or re-send an activation email
+           when a user does not receive or does not act upon the
+           initial activation email; since the account will be
+           deleted, the user will be able to simply re-register and
+           receive a new activation key.
+        
+        2. It prevents the possibility of a malicious user registering
+           one or more accounts and never activating them (thus
+           denying the use of those usernames to anyone else); since
+           those accounts will be deleted, the usernames will become
+           available for use again.
+        
+        If you have a troublesome ``User`` and wish to disable their
+        account while keeping it in the database, simply delete the
+        associated ``RegistrationProfile``; an inactive ``User`` which
+        does not have an associated ``RegistrationProfile`` will not
+        be deleted.
+        
+        """
+        for profile in self.all():
+            try:
+                if profile.activation_key_expired():
+                    user = profile.user
+                    if not user.is_active:
+                        user.delete()
+                        profile.delete()
+            except User.DoesNotExist:
+                profile.delete()
+
+
 class PendingUser(models.Model):
     # NOTE We might consider migrating the fields to CharField, which would
     # simplify form creation in forms.py
     first_name  = models.TextField()
     last_name   = models.TextField()
-    email       = models.EmailField(validators=[validate_email])
+    email       = models.EmailField() #validators=[validate_email])
     password    = models.TextField()
     keypair     = models.TextField()
     # institution
 
+    objects = RegistrationManager()
+
+    class Meta:
+        verbose_name = _('registration profile')
+        verbose_name_plural = _('registration profiles')
+    
+    def __unicode__(self):
+        return u"Registration information for %s" % self.user
+
+    def activation_key_expired(self):
+        """
+        Determine whether this ``RegistrationProfile``'s activation
+        key has expired, returning a boolean -- ``True`` if the key
+        has expired.
+        
+        Key expiration is determined by a two-step process:
+        
+        1. If the user has already activated, the key will have been
+           reset to the string constant ``ACTIVATED``. Re-activating
+           is not permitted, and so this method returns ``True`` in
+           this case.
+
+        2. Otherwise, the date the user signed up is incremented by
+           the number of days specified in the setting
+           ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of
+           days after signup during which a user is allowed to
+           activate their account); if the result is less than or
+           equal to the current date, the key has expired and this
+           method returns ``True``.
+        
+        """
+        expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
+        return self.activation_key == self.ACTIVATED or \
+               (self.user.date_joined + expiration_date <= datetime_now())
+    activation_key_expired.boolean = True
+
+    def send_activation_email(self, site):
+        """
+        Send an activation email to the user associated with this
+        ``RegistrationProfile``.
+        
+        The activation email will make use of two templates:
+
+        ``user_register_email_subject.txt``
+            This template will be used for the subject line of the
+            email. Because it is used as the subject line of an email,
+            this template's output **must** be only a single line of
+            text; output longer than one line will be forcibly joined
+            into only a single line.
+
+        ``user_register_email.txt``
+            This template will be used for the body of the email.
+
+        These templates will each receive the following context
+        variables:
+
+        ``activation_key``
+            The activation key for the new account.
+
+        ``expiration_days``
+            The number of days remaining during which the account may
+            be activated.
+
+        ``site``
+            An object representing the site on which the user
+            registered; depending on whether ``django.contrib.sites``
+            is installed, this may be an instance of either
+            ``django.contrib.sites.models.Site`` (if the sites
+            application is installed) or
+            ``django.contrib.sites.models.RequestSite`` (if
+            not). Consult the documentation for the Django sites
+            framework for details regarding these objects' interfaces.
+
+        """
+        ctx_dict = {'activation_key': self.activation_key,
+                    'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
+                    'site': site}
+        subject = render_to_string('user_register_email_subject.txt',
+                                   ctx_dict)
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+
+        message = render_to_string('user_register_email.txt',
+                                   ctx_dict)
+
+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [self.email])
+
+
+
+
 class PendingSlice(models.Model):
     slice_name  = models.TextField()