Restricting User Signups in Django

October 12, 2008. Filed under django 72

I'm working on a Django app that is getting gradually closer to some kind of release, but I want to be able to ramp things up slowly, starting with a few targeted users and iterating from there. I've previously been quite impressed with django-authopenid for supporting OpenID and traditional sign-up and sign-in, and wanted to continue using it, so I sat down to plot a devious scheme that would allow me to restrict user registration without tampering with the django-authopenid's internals.

The cornerstone of my approach is the AppInvite model, which allows the creation of signup codes with a limited number of uses.

class AppInvite(models.Model):
    password = models.CharField(max_length=10)
    max = models.IntegerField()
    current = models.IntegerField()

Then I modified the project's urls.py to override django-authopenid's signup page:

(r'account/signup/','views.restrict_signup'),
(r'account/',include('django_authopenid.urls')),

As mentioned, I didn't want to mangle django-authopenid's internals, because this is only a quick fix to a temporary problem, not an integral piece of code that will stay around forever.

Next I needed to write the restrict_signup view:

from django_authopenid.views import signup
from django.shortcuts import render_to_response
from models import AppInvite

def restrict_signup(request):
    "If posting, pass it directly to signup."
    if request.method == 'POST':
        return signup(request)

    extra = {}
    if request.GET.has_key("pw"):
        pw = request.GET['pw']
        ai = AppInvite.objects.filter(password=pw)
        if len(ai) != 0:
            ai = ai[0]
            if ai.current < ai.max:
                ai.current = ai.current + 1
                ai.save()
                return signup(request)
            else:
                extra['error'] = "The signup code '%s' has expired." % pw
        else:
            extra['error'] = "'%s' isn't a valid signup code." % pw
    return render_to_response("restrict_signup.html", extra)

It the request recieves a GET request, then it forces the user to authenticate with a signup code, but if it receives a POST request, then it proxies it forward to django_authopenid's signup view.

The restrict_signup view has a couple of minor flaws:

  1. it counts the number of times it present the blank registration form, as opposed to the actual number of users created.

  2. it could be circumvented by manually sending a POST request instead of a GET request.

For my purposes, I'm not overly worried if a few fewer people are let in, or if a couple of people circumvent the system to sneak in, so it is still a sufficient solution for me.

The last piece of the puzzle is the restrict_signup.html template.

{% extends "base.html" %}
{% block content %}
<div class="main">
  <h2> Please Enter Signup Code </h2>
  {% if error %}<p>{{ error }}</p>{% endif %}
  <form action="" method="GET">
  <label for="pw">Signup Code:</label>
  <input name="pw">
  <input type="submit" value="Check Code">
  </form>  
</div>
{% endblock %}

I'm sure it would be useful if this pattern could be abstracted (and extended a bit) into a pluggable application, but that would require a bit more thought about how to structure it. My thoughts on that are:

  1. Support for arbitrary signup codes, that optionally have a limited number of uses.

  2. Support limiting the number of sign ups per time period (100/day, etc).

  3. Have a parameter in settings.py that specifies the view with the registration form for users who send a correct signup code.

  4. Yep.. that's pretty much it.