Source

django-registration / registration / models.py

import datetime
import random
import re
import hashlib

from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db import transaction
from registration.template import render_to_string
from django.utils.translation import ugettext_lazy as _


SHA1_RE = re.compile('^[a-f0-9]{40}$')


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``.

        The registration profile will be deleted after successful
        activation and they key thus becomes invalid.

        """

        # 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.password = profile.new_password
                user.email_confirmed = True
                user.save()
                profile.delete()
                return user
        return False

    def create_inactive_user(self, username, email, password, site,
                             active_by_default=False, send_email=True,
                             **kwargs):
        """
        Create a new, by default inactive, ``User``, generate a
        ``RegistrationProfile`` and email its activation key to the
        ``User``, returning the new ``User``.

        Using the ``active_by_default`` parameter, you may chose to
        enable the account immediately. You can still request for an
        activation email to be sent out. The activation process
        in this case will then only cause the user's
        ``email_confirmed`` flag to be set.

        By default, an activation email will be sent to the new
        user. To disable this, pass ``send_email=False``.

        """
        new_user = User.objects.create_user(username, email, password)
        new_user.save()

        registration_profile = self.create_profile(new_user)
        # if the user is to be created "inactive", move his password
        # to the registration profile (will be applied during activation)
        if not active_by_default:
            registration_profile.new_password = new_user.password
            new_user.set_unusable_password()
            registration_profile.save()
            new_user.save()

        if send_email:
            registration_profile.send_activation_email(site, **kwargs)
        return new_user
    create_inactive_user = transaction.commit_on_success(create_inactive_user)

    def reset_password(self, user, email_template=None,
                       email_subject_template=None,
                       email_extra_context={}):
        """
        Mail a new password to a user and setup the confirmation process.
        """

        # Generate the confirmation code and the new password; we call
        # ``set_password`` on the user to have Django generate the hash,
        # but we do not save that change!
        new_password = User.objects.make_random_password(length=10)
        old_password = user.password
        user.set_password(new_password)
        profile = self.create_profile(user=user, new_password=user.password)
        user.password = old_password

        # prepare email rendering
        # TODO: there is some overlap with ``register()`` here. move to
        # a function.
        context = {'confirmation_key': profile.activation_key,
                   'new_password': new_password,
                   'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
                   'account': user}
        context.update(email_extra_context)

        # render subject
        subject = render_to_string(
            email_subject_template or "registration/restorepw_email_subject.txt",
            context)
        subject = ''.join(subject.splitlines()) # *must not* contain newlines
        # render message
        message = render_to_string(
            email_template or "registration/activation_email.txt", context)

        # send the email
        from django.core.mail import send_mail
        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])

        return profile
    reset_password = transaction.commit_on_success(reset_password)

    def create_profile(self, user, **kwargs):
        """
        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.

        If a profile already exists (can easily happen during as part
        of multiple uses of the "password reset" feature, a new profile
        will be created (although the old record is reused).
        """
        salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
        username = user.username
        if isinstance(username, unicode):
            username = username.encode('utf-8')
        activation_key = sha_constructor(salt+username).hexdigest()
        defaults = kwargs.copy()
        defaults['activation_key'] = activation_key
        profile, created = self.get_or_create(user=user, defaults=defaults)
        if not created:
            profile.activation_key = activation_key
            profile.date_created = datetime.datetime.utcnow()
            for key, value in kwargs.items():
                setattr(profile, key, value)
            profile.save()
        return profile

    def delete_expired_users(self):
        """
        Remove expired instances of ``RegistrationProfile``, and when
        appropriate 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 a usable password set; any user who is boith
        without a password and has an expired activation key will be
        deleted.

        Whether a User account is removed or not, the expired
        registration profile will be deleted in any case.

        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.

        """
        for profile in self.all():
            if profile.activation_key_expired():
                user = profile.user
                if not user.has_usable_password():
                    # Profile was used for account registration that
                    # didn't go through.
                    user.delete()
                else:
                    # Profile intended for a password reset, or user
                    # activated through different means, e.g. admin.
                    profile.delete()


class RegistrationProfile(models.Model):
    """
    A simple profile which stores an activation key for use during
    user account registration. It can also be used for password resets.

    Generally, you will not want to interact directly with instances
    of this model; the provided manager includes methods
    for creating and activating new accounts, as well as for cleaning
    out accounts which have never been activated.

    While it is possible to use this model as the value of the
    ``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do
    so. This model's sole purpose is to store data temporarily during
    account registration and activation.

    """
    user = models.OneToOneField(User, primary_key=True, verbose_name=_('user'))
    activation_key = models.CharField(_('activation key'), max_length=40)
    new_password = models.CharField(_('password'), max_length=128)
    date_created = models.DateTimeField(default=datetime.datetime.utcnow)

    objects = RegistrationManager()

    class Meta:
        db_table = 'auth_registration'
        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.date_created + expiration_date <= datetime.datetime.utcnow()
    activation_key_expired.boolean = True

    def send_activation_email(self, site, email_subject_template='registration/activation_email_subject.txt',
                              email_template='registration/activation_email.txt',
                              email_extra_context={},):
        """
        Send an activation email to the user associated with this
        ``RegistrationProfile``.

        The activation email will make use of two templates:

        ``registration/activation_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.

        ``registration/activation_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.

		  The paths to the templates can be customized using the
		  ``email_subject_template`` and ``email_template`` parameters.
        """

        ctx_dict = { 'activation_key': self.activation_key,
                     'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
                     'site': site, 'new_user': self.user }
        ctx_dict.update(email_extra_context)
        subject = render_to_string(email_subject_template, ctx_dict)
        # Email subject *must not* contain newlines
        subject = ''.join(subject.splitlines())

        message = render_to_string(email_template, ctx_dict)

        self.user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)


#
# Add new field ``email_confirmed`` to the ``User`` model. The idea is
# that while a site might want to enable user accounts instantly
# (is_active=True) instead of requiring activation, it might want to users
# rights to be limited until his email address has been confirmed through
# an activation process.
#

try:
    User._meta.get_field_by_name("email_confirmed")
except models.FieldDoesNotExist:
    User.add_to_class('email_confirmed',
        models.BooleanField(_('email confirmed'), default=False))
else:
    pass