Daniel Roy Greenfeld

Django GetOrCreateView

Tue Oct 16 2012 2 min read


Today I decided to use the Django class based view (CBV) CreateView, but I wanted to avoid duplications and submit to the view from the front page of a site. The reason was I needed a simple newsletter signup form. This is what I cooked up and should work for Django 1.3, 1.4, and the forthcoming 1.5 release. Here is what I did:

  1. Installed dependencies =========================

This version requires the following package to be pip installed into your virtualenv.

This also needs to be added to your list of INSTALLED_APPS:

INSTALLED_APPS += (
    'django_extensions',
)
  1. Defined the model ====================

The model is really simple, and inherits from TimeStampedModel so we know when people signed up:

from django.db import models

from django_extensions.db.models import TimeStampedModel

class NewsLetterSignup(TimeStampedModel):

    email = models.EmailField("Email")

    def __unicode__(self):
        return self.email
  1. Wrote the view =================

Here's the somewhat challenging part that forced me to dive into Django's source code. Even with the documentation work we've done over the past few months, it's clear we've got a long way to go.

Because of that source code diving, for this blog post I really did my best to document why I did things in the NewsLetterSignupView.form_valid() method.

from django.http import HttpResponseRedirect
from django.views.generic import CreateView

from .models import NewsLetterSignup

class NewsLetterSignupView(CreateView):
    """ Signs up users to a newsletter """

    model = NewsLetterSignup
    success_url = '/newsletter-signed-up/'  # replace with reverse

    def form_valid(self, form):
        """
        If the form is valid, save the associated model.
            (django.views.generic.edit.ModelFormMixin)
        If the form is valid, redirect to the supplied URL.
            (django.views.generic.edit.FormMixin)
        """

        # Get the email from the form.cleaned_data dictionary
        email = form.cleaned_data.get("email", "")

        # Get or create the signup. We don't need to do anything with the
        #   model instance or created boolean so we don't set them.
        NewsLetterSignup.objects.get_or_create(email=email)  

        # Don't use super() to inherit as it will do a form.save()
        # You could call the FormMixin's form_valid() method but I think    
        #   using a HttpResponseRedirect() much more explicit.
        return HttpResponseRedirect(self.success_url)  
  1. Wired it together ====================

In urls.py:

from django.conf.urls import patterns, url
from django.views.generic import TemplateView

from .views import NewsLetterSignupView

urlpatterns = patterns('',
    url(regex=r'^newsletter-signed-up/$',
        view=TemplateView.as_view(
            template_name="pages/newsletter_signed_up.html"
        ),
        name='newsletter_signedup',
    ),
    url(regex=r'^newsletter-signup/$',
        view=NewsLetterSignupView.as_view(),
        name='news_letter_signup',
    ),
)

# Closing thoughts

First off, you'll notice I didn't include the pages/newsletter_signed_up.html because for this case it's too trivial.

Second, this is one of those very clear cases where a functional view would have been so much easier compared to the effort I spent writing this as a class based view. The line count would have been about the same, but the mental bandwidth involved in figuring this would have been a fraction of the effort I spent.

Third, this is probably better served with an implementation of django.views.generic.FormView. Oh well...

Fourth, I want to see a configurable version of this in the next release of django-braces. 😉