Words

Previous   Next

Django Custom Model Field With Custom Admin Widget

Posted Jan 6 by shawn 17

Tagged withcodedevdjangogooglemapspython

I’d like to share a few things about writing your own custom model field for Django. I just finished building a field that allows a user to pick a location by embedding a google map into the admin site. I based my field off of the fields in localflavor of the Django code. The field saves serialized data to a standard text field in the database so I’ll use that as an example.

We’re going to write a custom widget so we can control the HTML that is rendered in the admin. To do this you can create a separate file with your html and javascript. I also placed most of the javascript into an external js file so that I could let Django worry about whether the file had already been loaded or not when I have a model that contains more than one of my custom fields.

In a file, we’ll call ours widgets.py, we add a new class:

class GeoLocationFieldWidget(AdminTextareaWidget):
  """ 
  The widget used to render the GeoLocationField 
  in the admin.
  Allows fine tuning of a geo-coded address with 
  a clickable google map.

  Assumes that the required js files are in the media js folder.
  * jquery.js
  * jquery.json-1.3.min.js
  * geo_location_field.js

  Also assumes that the google maps api key is 
  in the settings file under GOOGLE_MAPS_API_KEY.
  """

  def __init__(self, attrs=None):
    super(GeoLocationFieldWidget, self).__init__(attrs)

  def render(self, name, value, attrs=None):
    return render_to_string("geo_location_field.html", locals())

  class Media:
    try:
      js = ['js/jquery.js', 
            'js/geo_location_field.js', 
            'js/jquery.json-1.3.min.js',
            'http://maps.google.com/maps?file=api&v=2&key=' + settings.GOOGLE_MAPS_API_KEY]
    except AttributeError:
      pass

This file can live whereever you like, so long as your Django project can import it.

In this file all we need to do is define the render method, which in this case simply renders a Django template. This makes it much easier to add complicated layout and even do some scripting if you like. Notice I have a few js files that are needed, the downside to this approach is that you must remember to add them to your django projects media when you use this custom model field.

Also notice that the Google API key is expected to be in the settings file.

Now to get your field to use this widget you need to override the formfield method in your class and specify the widget keyword argument to use your new widget class, which in turn renders your html to a string. In a new file (we called ours fields.py) file for fields add a new class. There is nothing stopping you from doing this in the same file we just think it’s cleaner to separate them.

class GeoLocationField(models.TextField):
  """ 
  Stores an address and the google geocoded 
  data for latitude and longitude.

  This data is automatically retrieved 
  from google when the location is saved.
  """

  __metaclass__ = models.SubfieldBase

  def to_python(self, value):

    if not value or isinstance(value, dict):
      return value

    try:
      value = simplejson.JSONDecoder().decode(value)
    except ValueError:
      value = {}

    return value

  def get_db_prep_value(self, value):
    return simplejson.JSONEncoder().encode(value)

  def formfield(self, **kwargs):

    kwargs['widget'] = widgets.GeoLocationFieldWidget
    kwargs['form_class'] = forms.GeoLocationFormField

    return super(GeoLocationField, self).formfield(**kwargs)

Next we want to be sure we validate the data that is coming from the widget.

To do this, in that same formfield method, point the form_class keyword argument at a new form field class you create. Be sure you add a clean method in your form field class and raise a forms.ValidationError if things go wrong. If all is well, return the value you consider to be cleaned. Here is the formfield class from the forms.py file:

class GeoLocationFormField(fields.Field):
  """ 
  Used to validate the data submitted by
  the GeoLocationFieldWidget in the admin.
  """

  def clean(self, value):
    """ 
    Validates the json submitted and the required properties.
    """
    if super(GeoLocationFormField, self).clean(value):

      try:
        data = simplejson.JSONDecoder().decode(value)
      except ValueError:
        # This should never happen, but if by chance the front end builds bad json, we catch it here. The user has no options if this is happening, we will need to find the bug.
        raise forms.ValidationError('An error has occured. Unable to parse the geo encoded data for saving.')

      # Now make sure the json has all of the required properties to save.
      if not data['address'] or len(data['address']) < 1:
        raise forms.ValidationError('Please provide a location or address.')

      if not data['latitude'] or len(data['latitude']) < 1 or not data['longitude'] or len(data['longitude']) < 1:
        raise forms.ValidationError('Please wait for the geo encoding of your location to complete before clicking save.')

    return value

Now in your models you can just import this custom field and use it just the same as all of the built in Django fields. Nice!

As with anything there are definitely other ways of doing this, if anyone has any suggestions for improvement, we’d love to hear from you.

UPDATE: Here are the requested files ( right click to download ): geo_location_field.js geo_location_field.html

squareFACTOR
222 South Westmonte Dr.
Suite 311
Altamonte Springs, FL 32714 28.659606 -81.393275