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) |
As you can see we have just a single field, name. Now let’s create the modelform:
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 |
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:
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) |
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') |
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 |
Ensure the middleware is added to your application settings:
|
MIDDLEWARE_CLASSES = ( |
|
… |
|
'app.middleware.exceptions.ExceptionMiddleware', |
|
) |
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'), |
|
) |
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(); |
|
}); |
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.
Like this:
Like Loading...