Commits

Tino de Bruijn  committed 542647e

initial setup of django-user-creation

  • Participants

Comments (0)

Files changed (16)

+syntax: glob
+
+**.pyc
+.DS_Store
+## -------------------- USAGE -------------------- ##
+1. Copy the contents of 'templates' to a template folder in your project
+
+2. Adjust the activation_subject.txt and activation_email.txt to you wishes
+
+3. Append 'user_creation' to your INSTALLED_APPS setting
+
+4. Add 'from user_creation import useradmin' in you root urls.py *after* 'admin.autodiscover()'
+
+5. Add  
+    # user_creation
+    (r'^user_creation/', include('user_creation.urls')),
+   to your root urls.py
+   
+6. If your templates don't extend a site called "base_site.html", adjust "activate.html" accordingly.

File setup.py

Empty file added.

File user_creation/__init__.py

Empty file added.

File user_creation/forms.py

+from django.contrib.auth.models import User
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from models import ActivationProfile
+
+class AccountCreationForm(forms.Form):
+    """
+    A form that creates a user, with no privileges, from the given username and password.
+    """
+    username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
+        help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
+        error_message = _("This value must contain only letters, numbers and underscores."))
+    email = forms.EmailField(label=_("Email address"))
+    password = forms.CharField(label=_("Password"), required=False,
+        help_text = _("Leave blank to create a user with a random password and an activation profile. \
+            Only use this if you want to set up an account with a known password"))
+    email_user = forms.BooleanField(initial=True, label=_("Send the user a notification email"), required=False,
+        help_text = _("If you generate a random password, this will be an email with an activation link. Otherwise it will contain the users login credentials"))
+
+    def clean_username(self):
+        """ Validates that the username is alphanumeric and not already in use. """
+        username = self.cleaned_data["username"]
+        try:
+            User.objects.get(username=username)
+        except User.DoesNotExist:
+            return username
+        raise forms.ValidationError(_("A user with that username already exists."))
+        
+    def clean_email(self):
+        """ Validates that the email address is not already in use. """
+        email = self.cleaned_data["email"]
+        try:
+            User.objects.get(email=email)
+        except User.DoesNotExist:
+            return email
+        raise forms.ValidationError(_("A user with that email address already exists."))
+        
+    def save(self):
+        """ Creates a normal new user and return that, or creates an inactive user with an activation profile. """ 
+        send_email = self.cleaned_data["email_user"]
+        if self.cleaned_data["password"] == '': # a activation profile needs to be created
+            new_user = ActivationProfile.objects.create_inactive_user(self.cleaned_data["username"], 
+                                self.cleaned_data["email"], send_email=send_email)
+        else: # a normal user can be created
+            new_user = User.objects.create_user(self.cleaned_data["username"], 
+                                self.cleaned_data["email"], 
+                                self.cleaned_data["password"]
+                                )
+            new_user.save()
+        return new_user
+
+class SetPasswordForm(forms.Form):
+    """
+    A form that lets a user change or set his/her password without entering
+    the old password
+    """
+    new_password1 = forms.CharField(label=_("New password"), widget=forms.PasswordInput)
+    new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput)
+    activation_key = forms.CharField(widget=forms.HiddenInput, required=False)
+
+    def __init__(self, user, *args, **kwargs):
+        self.user = user
+        super(SetPasswordForm, self).__init__(*args, **kwargs)
+
+    def clean_new_password2(self):
+        password1 = self.cleaned_data.get('new_password1')
+        password2 = self.cleaned_data.get('new_password2')
+        if password1 and password2:
+            if password1 != password2:
+                raise forms.ValidationError(_("The two password fields didn't match."))
+        return password2
+
+    def save(self, commit=True):
+        self.user.set_password(self.cleaned_data['new_password1'])
+        if commit:
+            self.user.save()
+        return self.user

File user_creation/locale/nl/LC_MESSAGES/django.mo

Binary file added.

File user_creation/locale/nl/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-09-22 15:01+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:10
+msgid "Username"
+msgstr ""
+
+#: forms.py:11
+msgid ""
+"Required. 30 characters or fewer. Alphanumeric characters only (letters, "
+"digits and underscores)."
+msgstr ""
+
+#: forms.py:12
+msgid "This value must contain only letters, numbers and underscores."
+msgstr ""
+
+#: forms.py:13
+msgid "Email address"
+msgstr ""
+
+#: forms.py:14
+msgid "Password"
+msgstr ""
+
+#: forms.py:15
+msgid ""
+"Leave blank to create a user with a random password and an activation "
+"profile.             Only use this if you want to set up an account with a "
+"known password"
+msgstr ""
+"Laat leeg als je een gebruiker wilt maken met een random wachwoord en een "
+"activatie email. Gebruik dit alleen als je een account maakt met een bekend "
+"wachtwoord."
+
+#: forms.py:17
+msgid "Send the user a notification email"
+msgstr "Stuur de gebruiker een notificatie email"
+
+#: forms.py:18
+msgid ""
+"If you generate a random password, this will be an email with an activation "
+"link. Otherwise it will contain the users login credentials"
+msgstr ""
+
+#: forms.py:28
+msgid "A user with that username already exists."
+msgstr ""
+
+#: forms.py:37
+msgid "A user with that email address already exists."
+msgstr ""
+
+#: forms.py:58
+msgid "New password"
+msgstr ""
+
+#: forms.py:59
+msgid "New password confirmation"
+msgstr ""
+
+#: forms.py:71
+msgid "The two password fields didn't match."
+msgstr ""
+
+#: models.py:220
+msgid "user"
+msgstr ""
+
+#: models.py:221
+msgid "activation key"
+msgstr "activatie code"
+
+#: models.py:226
+msgid "activation profile"
+msgstr "activatie profiel"
+
+#: models.py:227
+msgid "activation profiles"
+msgstr "activatie profielen"
+
+
+#: views.py:26
+msgid ""
+"Your password has been set,                     please remember to remember "
+"it. You are now logged in."
+msgstr ""
+"Je wachtwoord is ingesteld. Vergeet 'm alsjeblieft niet te onthouden. Je "
+"bent nu ingelogd."
+
+#: views.py:30
+msgid "Tampered activation key"
+msgstr "Fout met de activatie code"
+
+#: templates/admin/auth/user/add_form.html:5
+msgid ""
+"First, enter a username and an email address. Then, you'll be able to edit "
+"more user options."
+msgstr ""
+"Vul eerst een gebruikersnaam en een email adres in. Daarna kun je de overige "
+"gebruikers-opties wijzigen."
+
+#: templates/user_creation/activate.html:4
+msgid "Account activation"
+msgstr "Account activatie"
+
+#: templates/user_creation/activate.html:8
+msgid "Account activated."
+msgstr "Account geactiveerd"
+
+#: templates/user_creation/activate.html:9
+msgid "Thanks for activating your account. Please set your password below."
+msgstr ""
+"Bedankt voor het activeren van je account. Stel alsjeblieft hieronder je "
+"wachtwoord in."
+
+#: templates/user_creation/activate.html:12
+msgid "Set password"
+msgstr "Stel wachtwoord in"
+
+#: templates/user_creation/activate.html:15
+msgid "Activation error"
+msgstr "Activatie fout"
+
+#: templates/user_creation/activate.html:16
+#, python-format
+msgid ""
+"\n"
+"    Sorry, it didn't work. Either you already activated, your activation \n"
+"    link was incorrect, or\n"
+"    the activation key for your account has expired; activation keys are\n"
+"    only valid for %(expiration_days)s days after registration.\n"
+"    <br/>Please contact the administrator."
+msgstr ""
+"\n"
+"Sorry, je kan niet geactiveerd worden. Of je bent al geactiveerd, of de "
+"activatielink was niet correct, of de activatie code is verlopen; activatie "
+"codes zijn %(expiration_days)s dagen geldig. <br/>Neem alsjeblieft contact "
+"op met de administrator."

File user_creation/models.py

+"""
+Based on James Bennett's django-registration.
+"""
+
+import datetime
+import random
+import re
+
+from django.conf import settings
+from django.db import models
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+from django.utils.hashcompat import sha_constructor
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.mail import send_mail
+
+
+SHA1_RE = re.compile('^[a-f0-9]{40}$')
+
+
+class ActivationManager(models.Manager):
+    """
+    Custom manager for the ``ActivationProfile`` 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 check_key(self, activation_key):
+        """
+        Checks to see if the activation_key exists.
+        """
+        
+        # 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():
+                return profile.user
+        return False
+        
+    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 ``ALREADY_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.delete()
+                return user
+        return False
+    
+    def create_inactive_user(self, username, email, password='invalid',
+                             send_email=True, profile_callback=None):
+        """
+        Create a new, inactive ``User``, (with an invalid password if none is
+        given), generates an ``ActivationProfile`` and email its activation
+        key to the ``User``, returning the new ``User``.
+        
+        To disable the email, call with ``send_email=False``.
+        
+        To enable creation of a custom user profile along with the
+        ``User`` (e.g., the model specified in the
+        ``AUTH_PROFILE_MODULE`` setting), define a function which
+        knows how to create and save an instance of that model with
+        appropriate default values, and pass it as the keyword
+        argument ``profile_callback``. This function should accept one
+        keyword argument:
+
+        ``user``
+            The ``User`` to relate the profile to.
+        
+        """
+        new_user = User.objects.create_user(username, email, password)
+        if password == 'invalid':
+            new_user.password = 'invalid' # no salt, no encoding, so invalid
+        new_user.is_active = False
+        new_user.save()
+        activation_profile = self.create_profile(new_user)
+        if profile_callback is not None:
+            profile_callback(user=new_user)
+        if send_email:
+            if self.password != 'invalid':
+                self.mail_activation_link(new_user, activation_profile)
+            else:
+                self.mail_credentials(new_user, password)
+        return new_user
+    
+    def create_profile(self, user):
+        """
+        Create an ``ActivationProfile`` for a given
+        ``User``, and return the ``ActivationProfile``.
+        
+        The activation key for the ``ActivationProfile`` will be a
+        SHA1 hash, generated from a combination of the ``User``'s
+        username and a random salt.
+        
+        """
+        salt = sha_constructor(str(random.random())).hexdigest()[:5]
+        activation_key = sha_constructor(salt+user.username).hexdigest()
+        return self.create(user=user,
+                           activation_key=activation_key)
+        
+    def delete_expired_users(self):
+        """
+        Remove expired instances of ``ActivationProfile``.
+        
+        Profiles older than ``ACTIVATION_KEY_EXPIRY`` setting will be removed.
+        """
+        oldest = datetime.datetime.now() - \
+            datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
+        self.filter(creation_date__lt=oldest).delete()
+                
+    def mail_activation_link(self, user, activation_profile):
+        """
+        Sends the activation link to the users email.
+        """
+        current_site = Site.objects.get_current()
+        
+        subject = render_to_string('accounts/activation_email_subject.txt',
+                                   { 'site': current_site })
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+        
+        message = render_to_string('accounts/activation_email.txt',
+                                   { 'activation_key': activation_profile.activation_key,
+                                     'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
+                                     'site': current_site })
+        
+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])
+    
+    def mail_credentials(self, user, password):
+        """
+        Sends the credentials to the users email.
+        """
+        current_site = Site.objects.get_current()
+        
+        subject = render_to_string('accounts/activation_email_subject.txt',
+                                   { 'site': current_site })
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+        
+        message = render_to_string('accounts/credentials_email.txt',
+                                   { 'username' : user.username,
+                                     'password': password,
+                                     'site': current_site })
+        
+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])
+
+class ActivationProfile(models.Model):
+    """
+    A simple profile which stores an activation key for use during
+    user account activation.
+    
+    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, and a mechanism for
+    automatically creating an instance of a site-specific profile
+    model is provided via the ``create_inactive_user`` on
+    ``RegistrationManager``.
+    
+    """
+    user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
+    activation_key = models.CharField(_('activation key'), max_length=40)
+    creation_date = models.DateField(default=datetime.datetime.now)
+    
+    objects = ActivationManager()
+    
+    class Meta:
+        verbose_name = _('activation profile')
+        verbose_name_plural = _('activation profiles')
+    
+    def __unicode__(self):
+        return u"Activation information for %s" % self.user
+    
+    def activation_key_expired(self):
+        """
+        Determine whether this ``ActivationProfile``'s activation
+        key has expired, returning a boolean -- ``True`` if the key
+        has expired.
+        
+        The date the user signed up is incremented by
+        the number of days specified in the setting
+        ``ACTIVATION_KEY_EXPIRY`` (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.user.date_joined + expiration_date <= datetime.datetime.now()
+    activation_key_expired.boolean = True

File user_creation/templates/admin/auth/user/add_form.html

+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+{% block after_field_sets %}
+<p>{% trans "First, enter a username and an email address. Then, you'll be able to edit more user options." %}</p>
+
+<fieldset class="module aligned">
+
+<div class="form-row">
+  {{ form.username.errors }}
+  <label for="id_username" class="required">{{ form.username.label }}</label> {{ form.username }}
+  <p class="help">{{ form.username.help_text }}</p>
+</div>
+
+<div class="form-row">
+  {{ form.email.errors }}
+  <label for="id_email" class="required">{{ form.email.label }}</label> {{ form.email }}
+  <p class="help">{{ form.email.help_text }}</p>
+</div>
+
+<div class="form-row">
+  {{ form.password.errors }}
+  <label for="id_password">{{ form.password.label }}</label> {{ form.password }}
+  <p class="help">{{ form.password.help_text }}</p>
+</div>
+
+<div class="form-row">
+    {{ form.email_user }}
+    <label for="id_skip_emailing_user">{{ form.email_user.label }}</label>
+    <p class="help">{{ form.email_user.help_text }}</p>
+</div>
+
+<script type="text/javascript">document.getElementById("id_username").focus();</script>
+
+</fieldset>
+{% endblock %}

File user_creation/templates/user_creation/activate.html

+{% extends "base_site.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Account activation" %}{% endblock %}
+
+{% block content %}
+  {% if account %}
+  <h1>{% trans "Account activated." %}</h1>
+    <p>{% trans "Thanks for activating your account. Please set your password below." %}</p>
+    <form method="POST" action="">
+        {{set_password_form.as_p}}
+        <p><input type="submit" name="_save" value="{% trans "Set password" %}" /></p>
+    </form>
+  {% else %}
+  <h1>{% trans "Activation error" %}</h1>
+    <p>{% blocktrans %}
+    Sorry, it didn't work. Either you already activated, your activation 
+    link was incorrect, or
+    the activation key for your account has expired; activation keys are
+    only valid for {{ expiration_days }} days after registration.
+    <br/>Please contact the administrator.{% endblocktrans %}</p>
+  {% endif %}
+{% endblock %}

File user_creation/templates/user_creation/activation_email.txt

+Hi,
+
+An account has been created for you on Example.com
+
+You can activate your account by clicking on the following link (or copy it into your webbrowser):
+
+http://example.com/accounts/activate/{{ activation_key }}/
+
+See you there!
+

File user_creation/templates/user_creation/activation_email_subject.txt

+Activate your account @ Example.com

File user_creation/templates/user_creation/credentials_email.txt

+Hi,
+
+An account has been created for you on Example.com
+
+You can log into your account with the following credentials:
+
+username: {{ username }}
+password: {{ password }}
+
+See you there!
+

File user_creation/urls.py

+"""
+URLConf for Django user creation and authentication.
+
+Recommended usage is a call to ``include()`` in your project's root
+URLConf to include this URLConf for any URL beginning with
+``/accounts/``.
+
+"""
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+    url(r'^activate/(?P<activation_key>\w+)/$', 'views.activate', name='activate'),
+)

File user_creation/useradmin.py

+from django.contrib.auth.models import User
+from django.contrib import admin
+from forms import AccountCreationForm
+
+"""
+Override the account creation form in the admin.
+"""
+
+UserAdmin = admin.site._registry[User]
+UserAdmin.add_form = AccountCreationForm
+UserAdmin.save_on_top = True

File user_creation/views.py

+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from django.contrib.auth import login, authenticate
+from forms import SetPasswordForm
+from models import ActivationProfile
+
+def activate(request, activation_key, template_name='user_creation/activate.html'):
+    """
+    Activates an account if the given key exists and if a password is given through the form.
+    """
+    activation_key = activation_key.lower() # Normalize before trying anything with it.
+    if request.method == 'POST':
+        form = SetPasswordForm('', request.POST)
+        account = True
+        if form.is_valid():
+            account = ActivationProfile.objects.activate_user(form.cleaned_data['activation_key'])
+            if account:
+                form.user = account
+                form.save()
+                user = authenticate(username=account.username, password=form.cleaned_data['new_password1'])
+                login(request, user)
+                request.user.message_set.create(message=_("Your password has been set, \
+                    please remember to remember it. You are now logged in."))
+                return HttpResponseRedirect(settings.get('LOGIN_REDIRECT_URL','/'))
+            else:
+                form.error_messages={'error': _('Tampered activation key')}
+    else:
+        account = ActivationProfile.objects.check_key(activation_key)
+        form = SetPasswordForm(account, initial={'activation_key':activation_key})
+    return render_to_response(template_name,
+                              { 'account': account,
+                                'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
+                                'set_password_form':form })