Hi, I'm Daniel Greenfeld, and welcome to my blog. I write about Python, Django, and much more.

Django GetOrCreateView

Tuesday, October 16, 2012 (permalink)

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',
)

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

3. 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)

4. 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 django.views.generic.FormView. Oh well...

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


Comments