2009-05-13 05:48:10 +01:00
|
|
|
import datetime
|
|
|
|
import random
|
|
|
|
import re
|
2009-05-22 20:59:03 +01:00
|
|
|
import hashlib
|
2009-05-13 05:48:10 +01:00
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.auth.models import User
|
|
|
|
from django.contrib.sites.models import Site
|
|
|
|
from django.db import models
|
|
|
|
from django.db import transaction
|
|
|
|
from django.template.loader 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``.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
To execute customized logic when a ``User`` is activated,
|
|
|
|
connect a function to the signal
|
|
|
|
``registration.signals.user_activated``; this signal will be
|
|
|
|
sent (with the ``User`` as the value of the keyword argument
|
|
|
|
``user``) after a successful activation.
|
|
|
|
|
|
|
|
"""
|
|
|
|
from registration.signals import user_activated
|
|
|
|
|
|
|
|
# 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()
|
|
|
|
user_activated.send(sender=self.model, user=user)
|
|
|
|
return user
|
|
|
|
return False
|
|
|
|
|
|
|
|
def create_inactive_user(self, username, password, email,
|
|
|
|
send_email=True):
|
|
|
|
"""
|
|
|
|
Create a new, inactive ``User``, generate a
|
|
|
|
``RegistrationProfile`` and email its activation key to the
|
|
|
|
``User``, returning the new ``User``.
|
|
|
|
|
|
|
|
To disable the email, call with ``send_email=False``.
|
|
|
|
|
|
|
|
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. It receives one context variable, ``site``, which
|
|
|
|
is the currently-active
|
|
|
|
``django.contrib.sites.models.Site`` instance. 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. It
|
|
|
|
will receive three context variables: ``activation_key``
|
|
|
|
will be the user's activation key (for use in constructing
|
|
|
|
a URL to activate the account), ``expiration_days`` will
|
|
|
|
be the number of days for which the key will be valid and
|
|
|
|
``site`` will be the currently-active
|
|
|
|
``django.contrib.sites.models.Site`` instance.
|
|
|
|
|
|
|
|
To execute customized logic once the new ``User`` has been
|
|
|
|
created, connect a function to the signal
|
|
|
|
``registration.signals.user_registered``; this signal will be
|
|
|
|
sent (with the new ``User`` as the value of the keyword
|
|
|
|
argument ``user``) after the ``User`` and
|
|
|
|
``RegistrationProfile`` have been created, and the email (if
|
|
|
|
any) has been sent..
|
|
|
|
|
|
|
|
"""
|
|
|
|
from registration.signals import user_registered
|
|
|
|
|
|
|
|
new_user = User.objects.create_user(username, email, password)
|
|
|
|
new_user.is_active = False
|
|
|
|
new_user.save()
|
|
|
|
|
|
|
|
registration_profile = self.create_profile(new_user)
|
|
|
|
|
|
|
|
if send_email:
|
2009-05-22 20:59:03 +01:00
|
|
|
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
|
|
|
2009-05-13 05:48:10 +01:00
|
|
|
current_site = Site.objects.get_current()
|
|
|
|
|
|
|
|
subject = render_to_string('registration/activation_email_subject.txt',
|
2009-05-13 06:15:48 +01:00
|
|
|
{ 'site': settings.URL_ROOT })
|
2009-05-13 05:48:10 +01:00
|
|
|
# Email subject *must not* contain newlines
|
|
|
|
subject = ''.join(subject.splitlines())
|
|
|
|
|
2009-05-22 20:59:03 +01:00
|
|
|
text_content = render_to_string('registration/activation_email.txt',
|
2009-05-13 05:48:10 +01:00
|
|
|
{ 'activation_key': registration_profile.activation_key,
|
|
|
|
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
|
2009-05-13 06:15:48 +01:00
|
|
|
'site': settings.URL_ROOT })
|
2009-05-22 20:59:03 +01:00
|
|
|
html_content = render_to_string('registration/activation_email.html',
|
|
|
|
{ 'activation_key': registration_profile.activation_key,
|
|
|
|
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
|
|
|
|
'site': settings.URL_ROOT })
|
|
|
|
msg = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [new_user.email])
|
|
|
|
msg.attach_alternative(html_content, "text/html")
|
|
|
|
msg.send()
|
|
|
|
# send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [new_user.email])
|
2009-05-13 05:48:10 +01:00
|
|
|
user_registered.send(sender=self.model, user=new_user)
|
|
|
|
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.
|
|
|
|
|
|
|
|
"""
|
2009-05-22 20:59:03 +01:00
|
|
|
salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
|
|
|
|
activation_key = hashlib.sha1(salt+user.username).hexdigest()
|
2009-05-13 05:48:10 +01:00
|
|
|
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():
|
|
|
|
if profile.activation_key_expired():
|
|
|
|
user = profile.user
|
|
|
|
if not user.is_active:
|
|
|
|
user.delete()
|
|
|
|
|
|
|
|
|
|
|
|
class RegistrationProfile(models.Model):
|
|
|
|
"""
|
|
|
|
A simple profile which stores an activation key for use during
|
|
|
|
user account registration.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
"""
|
|
|
|
ACTIVATED = u"ALREADY_ACTIVATED"
|
|
|
|
|
|
|
|
user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
|
|
|
|
activation_key = models.CharField(_('activation key'), max_length=40)
|
|
|
|
|
|
|
|
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.datetime.now())
|
|
|
|
activation_key_expired.boolean = True
|