With a little hackery, it's quite doable.
In the following example, County is used instead of State and Municipality instead of City. So, the models are as follows:
class County(models.Model):
name = models.CharField(_('Name'), max_length=100, unique=True)
class Municipality(models.Model):
county = models.ForeignKey(County, verbose_name=_('County'))
name = models.CharField(_('Name'), max_length=100)
class Location(models.Model):
name = models.CharField(max_length=100)
county = models.ForeignKey(County, verbose_name=_('County'))
municipality = models.ForeignKey(Municipality,
verbose_name=_("Municipality"))
There are two sides of the problem: client-side JavaScript and server side field rendering.
The client side JavaScript (with JQuery, assumed to be served from /site_media/js/municipality.js) is as follows:
var response_cache = {};
function fill_municipalities(county_id) {
if (response_cache[county_id]) {
$("#id_municipality").html(response_cache[county_id]);
} else {
$.getJSON("/municipalities_for_county/", {county_id: county_id},
function(ret, textStatus) {
var options = '<option value="" selected="selected">---------</option>';
for (var i in ret) {
options += '<option value="' + ret[i].id + '">'
+ ret[i].name + '</option>';
}
response_cache[county_id] = options;
$("#id_municipality").html(options);
});
}
}
$(document).ready(function() {
$("#id_county").change(function() { fill_municipalities($(this).val()); });
});
Now you need the Ajax view for serving municipalities that belong to a given county (assumed to be served from /municipalities_for_county/):
from django.http import JSONResponse
from django.utils.encoding import smart_unicode
from django.utils import simplejson
from myproject.places.models import Municipality
def municipalities_for_county(request):
if request.is_ajax() and request.GET and 'county_id' in request.GET:
objs = Municipality.objects.filter(county=request.GET['county_id'])
return JSONResponse([{'id': o.id, 'name': smart_unicode(o)}
for o in objs])
else:
return JSONResponse({'error': 'Not Ajax or no GET'})
And finally the server side code in admin.py for rendering the field is as follows.
First, the imports:
from django import forms
from django.forms import widgets
from django.forms.util import flatatt
from django.utils.encoding import smart_unicode
from django.utils.safestring import mark_safe
from django.contrib import admin
from django.utils.translation import ugettext_lazy
from myproject.places.models import Municipality, Location
Then, the widget:
class MunicipalityChoiceWidget(widgets.Select):
def render(self, name, value, attrs=None, choices=()):
self.choices = [(u"", u"---------")]
if value is None:
# if no municipality has been previously selected,
# render either an empty list or, if a county has
# been selected, render its municipalities
value = ''
model_obj = self.form_instance.instance
if model_obj and model_obj.county:
for m in model_obj.county.municipality_set.all():
self.choices.append((m.id, smart_unicode(m)))
else:
# if a municipality X has been selected,
# render only these municipalities, that belong
# to X's county
obj = Municipality.objects.get(id=value)
for m in Municipality.objects.filter(county=obj.county):
self.choices.append((m.id, smart_unicode(m)))
# copy-paste from widgets.Select.render
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
return mark_safe(u'
'.join(output))
Next, the form:
class LocationForm(forms.ModelForm):
municipality = forms.ModelChoiceField(Municipality.objects,
widget=MunicipalityChoiceWidget(),
label=ugettext_lazy("Municipality"), required=False)
class Meta:
model = Location
def __init__(self, *args, **kwargs):
"""
We need access to the county field in the municipality widget, so we
have to associate the form instance with the widget.
"""
super(LocationForm, self).__init__(*args, **kwargs)
self.fields['municipality'].widget.form_instance = self
And finally, the admin class:
class LocationAdmin(admin.ModelAdmin):
form = LocationForm
class Media:
js = ('http://ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.min.js',
'/site_media/js/municipality.js')
admin.site.register(Location, LocationAdmin)
Let me know if something remains unclear.