Populate and Validate a Django ModelForm using API Data with the Generic CreateView

I ran into a slightly tricky use case the other day wherein I needed to grab some data from an API, validate it against a Django ModelForm, and only show the form to the user if there are errors during the validation.

If the form is shown, the error messages from the validation should appear along with a pre-populated form (using the data from the API). Otherwise, the ModelForm should submit and be saved immediately, without any user interaction.

After digging through Django’s code it became clear that to accomplish this, I needed to override a couple methods of the generic class-based CreateView.

Here’s the code for the view…

from django.core.urlresolvers import reverse_lazy
from django.views.generic import CreateView
from app.forms import MyModelForm
from app.lib.api import API
class MyCreateView(CreateView):
template_name = 'form.html'
form_class = MyModelForm
success_url = reverse_lazy('home')
object = None
# allow data to be passed to this
# pulled from django/views/generic/edit.py/ModelFormMixin
def get_form_kwargs(self, data=None):
kwargs = super(MyCreateView, self).get_form_kwargs()
# if we have data, set the form's data so that we can auto-submit this form (without POSTing)
if data:
kwargs.update({'initial': data, 'data': data})
return kwargs
def get(self, request, *args, **kwargs):
# grab the data from the API
data = API.get_data()
formdata = {'user_id': data['user_id'],
'email': data['email'],
'first_name': data['first_name'],
'last_name': data['last_name']}
form_class = self.get_form_class()
# must instantiate the class directly and include the data object rather than use get_form
# will set both the initial and instance data
#form = self.get_form(form_class)
form = form_class(**self.get_form_kwargs(formdata))
# do we have a valid form submission
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
# once the user submits the form, validate the form and create the new user
def post(self, request, *args, **kwargs):
# setup the form
# we can use get_form this time as we no longer need to set the data property
form_class = self.get_form_class()
form = self.get_form(form_class)
# grab the data from the API
data = API.get_data()
form.initial = {'user_id': data['user_id'],
'email': data['email'],
'first_name': data['first_name'],
'last_name': data['last_name']}
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)

view raw
view.py
hosted with ❤ by GitHub

As you can see, the first thing I’m doing is overriding get_form_kwargs so that it can take a new argument – data. This allows me to pass in the data pulled from the API to the form. Since I want the form to be saved immediately, without any user interaction, I set both the initial and data properties to the passed in data.

The get method is now a simple exercise in pinging the API for data, placing it into a dictionary the form can use, and calling form.is_valid() to see if the data passes the validation.

If it doesn’t, the form is rendered (complete with validation errors and pre-populated data) and the post method handles when the form is submitted by the user.

Advanced Django Class-based Views, ModelForms and Ajax Example Tutorial

Reading the Django docs for working with class-based views, modelforms, and ajax left me with more than a few questions on how to properly architect them. After much trial and error I’ve landed on what I think is a good, re-usable structure.

The following will walk through a fairly complex example that uses modelforms, an ajax response mixin, some ajax helper methods, and exception middleware.

Let’s start off with a basic model:

from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)

view raw
model.py
hosted with ❤ by GitHub

As you can see we have just a single field, name. Now let’s create the modelform:

from django import forms
from django.forms import ModelForm
from app.models.author import Author
class UpdateAuthorForm(ModelForm):
name = forms.CharField(
max_length=Author._meta.get_field('name').max_length
)
def save(self, commit=True):
instance = super(UpdateAuthorForm, self).save(commit=False)
if commit:
# save
instance.save(update_fields=['name'])
return instance
class Meta:
model = Author
fields = ('name',)

view raw
modelform.py
hosted with ❤ by GitHub

The reason I’m created my own modelform rather than allowing Django to auto-generate one for me is that I’d like to set the max_length parameter on the field and control which fields are updated when the model is saved. This comes in very handy when working with larger models where you only want to write a subset of fields to the database.

It’s worth noting here that even though I’ve specified only the name field in the Meta class’s fields, Django will still update all fields when saving the model unless you explicitly declare the fields to update using update_fields.

Next is the view:

from django.views.generic import UpdateView
from app.views.mixins.ajaxformresponse import AjaxFormResponseMixin
from app.forms.author import UpdateAuthorForm
from app.lib.shortcuts import get_object_or_json404
from app.models.author import Author
class AjaxAuthorUpdateView(AjaxFormResponseMixin, UpdateView):
form_class = UpdateAuthorForm
def get_object(self, queryset=None):
return get_object_or_json404(Author, pk=self.kwargs['pk'])
def get_context_data(self, context):
context['success'] = True
return context

view raw
view.py
hosted with ❤ by GitHub

The view is doing a couple of very important things:

  • A mixin is being used to add ajax functionality to the view (more on this below)
  • The form_class is set to our model form
  • The object is fetched using a custom helper function called get_object_or_json404 (more on this below)
  • And a success parameter is set on the context (for when a form is successfully saved)  (you could add any additional values you would like including values from object instance using self.object – i.e. context[‘name’] = self.object.name)

You could also require authentication for this view by using this mixin.

Let’s look at the mixin:

from app.lib.shortcuts import render_to_json_response
# a mixin to add AJAX support to a form
# must be used with an object-based FormView (e.g. CreateView)
class AjaxFormResponseMixin(object):
def form_invalid(self, form):
return render_to_json_response(form.errors, status=400)
def form_valid(self, form):
# save
self.object = form.save()
# initialize an empty context
context = {}
# return the context as json
return render_to_json_response(self.get_context_data(context))

view raw
ajaxformresponse.py
hosted with ❤ by GitHub

This is a simple mixin that overrides the default form_invalid and form_valid functions to return json instead of their usual HttpResponse and HttpResponseRedirect, respectively. A couple of key things are happening in this mixin:

  • Another helper function called render_to_json_response is used to generate the json (more on this below)
  • The form_invalid method returns a 400 status code along with the form errors (it does not include the context)
  • The form_valid method saves the form and returns the context as json using the get_context_data method that was created in the view above

Now let’s have a look at the helper methods:

import json
from django.shortcuts import _get_queryset
from django.http import HttpResponse
from app.exceptions.custom import JsonNotFound
# replacement for django.shortcuts.get_object_or_404()
# allows json to be returned with a 404 error
def get_object_or_json404(klass, *args, **kwargs):
queryset = _get_queryset(klass)
try:
return queryset.get(*args, **kwargs)
except queryset.model.DoesNotExist:
raise JsonNotFound()
def render_to_json_response(context, **response_kwargs):
# returns a JSON response, transforming 'context' to make the payload
response_kwargs['content_type'] = 'application/json'
return HttpResponse(convert_context_to_json(context), **response_kwargs)
def convert_context_to_json(context):
# convert the context dictionary into a JSON object
# note: this is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects — such as Django model instances or querysets
# — can be serialized as JSON.
return json.dumps(context)

view raw
shortcuts.py
hosted with ❤ by GitHub

The first method, get_object_or_json404, is a replacement for django.shortcuts.get_object_or_404() and allows json to be returned with a 404 error. It makes use of a custom exception, JsonNotFound, which we’ll look at shortly.

The second helper method, render_to_json_response, converts the context to json (using a third helper method, convert_context_to_json), sets the content type to application/json, and returns everything using HttpResponse.

The custom exception allows us to trap record not found errors and return an error message as json as well. Here’s the exception:

class JsonNotFound(Exception):
def __init__(self):
Exception.__init__(self, 'Record not found')

view raw
exceptions.py
hosted with ❤ by GitHub

And here’s the middleware necessary to include the custom exception in your application (you don’t have to get this fancy with an error code and timestamp, but the 404 status and descriptive error message are a must):

from django.utils import timezone
from django.utils.dateformat import format
from django.conf import settings
from app.exceptions.custom import JsonNotFound
from app.lib.shortcuts import render_to_json_response
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
if type(exception) == JsonNotFound:
now = format(timezone.now(), u'U')
kwargs = {}
response = {
'status': '404',
'message': 'Record not found',
'timestamp': now,
'errorcode': settings.API_ERROR_RECORD_NOT_FOUND
}
return render_to_json_response(response, status=404, **kwargs)
return None

view raw
exceptions.py
hosted with ❤ by GitHub

Ensure the middleware is added to your application settings:

MIDDLEWARE_CLASSES = (
'app.middleware.exceptions.ExceptionMiddleware',
)

view raw
settings.py
hosted with ❤ by GitHub

Finally, let’s create a new route for our class-based view (in urls.py):

urlpatterns += patterns('',
url(r'author/(?P<pk>\d+)/$', AjaxAuthorUpdateView.as_view(), name='ajax_author_update_view'),
)

view raw
urls.py
hosted with ❤ by GitHub

You can now post to your view and receive a json response with a corresponding status code (400, 404, or 200 – and 401 if you use the authentication mixin). Here’s some sample javascript to get you going:

if((typeof Ajax) == 'undefined'){
var Ajax = {};
}
Ajax.post = function(caller, form, data)
{
if(typeof data == 'undefined') data = {};
var url = $(form).data('ajax-action'),
path = window.location.pathname,
ajax_req = $.ajax({
url: url,
type: 'POST',
data: data,
success: function(data, textStatus, jqXHR) {
if(data.success){
caller.success(data, textStatus, jqXHR);
} else {
caller.ajax_error();
}
},
error: function(data, textStatus, jqXHR) {
if(data.status == 400){
caller.ajax_error('400');
} else if(data.status == 401){
window.location.href = '/signin/?go=' + path;
} else if(data.status == 404){
caller.ajax_error('404');
} else {
caller.ajax_error();
}
}
});
};
if((typeof Author) == 'undefined'){
var Author = {};
}
Author.init = function()
{
var data = {
name: $('myform').find('#id_name').val()
};
Ajax.post(this, 'myform', data);
};
Author.success = function(data, textStatus, jqXHR)
{
console.log(data);
};
Author.ajax_error = function(etype)
{
console.log('error');
};
$(function() {
Author.init();
});

view raw
ajax.js
hosted with ❤ by GitHub

Lots to digest but hopefully this is straightforward and easy to implement. If you have any suggestions for improvements, please let me know in the comments.

Django ModelForm and Conditionally Disabled (Readonly) Fields

I ran into a use case today that required a model form to have some fields disabled (readonly), but only when a certain condition was met (specifically when a property of the model was set to true) . Since the model form is used with the fields both enabled and disabled, simply duplicating the model form and removing the necessary fields wasn’t an option (plus that’s not exactly DRY now is it).

The solution ended up being quite simple. First, you’ll need to override the clean method for each field that needs to be disabled within your model form class:

# the following fields can only be updated when the is_disabled property is set to false
def clean_field_1(self):
if self.instance.is_disabled:
return self.instance.field_1
else:
return self.cleaned_data.get('field_1')
def clean_field_2(self):
if self.instance.is_disabled:
return self.instance.field_2
else:
return self.cleaned_data.get('field_2')

view raw
forms.py
hosted with ❤ by GitHub

The clean field method simply checks the is_disabled property and if it’s true, returns the existing field data. It only returns the new submitted data, pulled from cleaned_data, if is_disabled is false.

The second part of the solution is to set the field to readonly in your template. Note that you need to use readonly rather than disabled as the field won’t actually be submitted if set to disabled (more info on this here). This will cause Django to throw a missing field error before the clean field method ever runs.

You can add the necessary html to the form field via the widget’s attributes property:

modelform.fields['field_1'].widget.attrs['readonly'] = True

view raw
views.py
hosted with ❤ by GitHub

Django ModelForms and request.user

Took a little bit of digging to figure this one out. If you’re using a ModelForm along with django.contrib.auth.models.User (or a customized User model), you’ll want to use the instance that is sitting in the request rather than allowing the ModelForm to create a new instance.

To do this, simply override the get_form_kwargs method in the view:

class MyView(UpdateView):
template_name = 'user.html'
form_class = UserForm
success_url = reverse_lazy('success')
def get_form_kwargs(self):
kwargs = super(MyView, self).get_form_kwargs()
kwargs['instance'] = self.request.user
return kwargs

view raw
views.py
hosted with ❤ by GitHub

Subclass a Django Model Form and Add Extra Fields

A common use case of forms is to subclass some type of base form and add a couple extra fields. Take the following base form for example:

from django import forms
from app.models import User
from app.forms.widgets import *
class RegisterBaseForm(forms.ModelForm):
username = forms.CharField(max_length=User._meta.get_field('username').max_length)
email = forms.CharField(max_length=User._meta.get_field('email').max_length, widget=HTML5EmailInput())
password = forms.CharField(min_length=6, max_length=16, widget=forms.PasswordInput())
class Meta:
model = User
fields = ('username', 'email', 'password')

view raw
forms.py
hosted with ❤ by GitHub

The model has a variety of other fields in it so I’m using fields in the Meta class to limit to just these three. Now let’s add a couple fields in a subclass:

from django import forms
from base import *
from app.models import User
class RegisterEmailForm(RegisterBaseForm):
first_name = forms.CharField(max_length=User._meta.get_field('first_name').max_length)
last_name = forms.CharField(max_length=User._meta.get_field('last_name').max_length)
class Meta(RegisterBaseForm.Meta):
fields = RegisterBaseForm.Meta.fields + ('first_name', 'last_name')

view raw
forms.py
hosted with ❤ by GitHub

As you can see, rather than re-specifying all the fields in the Meta class I am simply adding the new fields to the parent’s existing field list.

Using Your Django Model’s max_length in Your ModelForm

Let’s say you have a field defined as the following in your model:

class User(models.Model):
first_name = models.CharField(max_length=40, null=True)

view raw
models.py
hosted with ❤ by GitHub

You can then use the max_length in your ModelForm like this:

class RegisterForm(forms.ModelForm):
first_name = forms.CharField(max_length=User._meta.get_field('first_name').max_length)

view raw
forms.py
hosted with ❤ by GitHub