
Saving GeoPoints with Django Form
A while ago I was working on a project where I integrated a map into a straightforward model form. Users could select a point on the map and submit it. The form’s task was to receive the data, validate it, and if all was well, store it in the database. I utilized MySQL with GIS support for this task. Along the way, I encountered a few hurdles that I’ll deep dive into here, explaining how I resolved them.
Alright, Let’s begin. Consider the below example
from django.contrib.gis.db import models
class Location(models.Model):
coordinate = models.PointField(blank=True, null=True)
# ... many more fields
If you examine the Django-generated migration file for this model, you’ll observe that the default value of the srid
parameter is set to 4326, even though we never explicitly provided that in the model definition.
operations = [
migrations.CreateModel(
name='Location',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('coordinate', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)),
],
),
]
The default value of srid
is being propagated from the base classBaseSpatialField
, the parent class of PointField
. While we have the flexibility to adjust this value based on our needs, the default usually serves well for most cases.
Let’s try to save some Geo coordinates through the shell. To get started, let’s import the Point class so that we can directly assign the value to the model field. Go ahead and fire up the Python shell using python manage.py shell
.
>>> from django.contrib.gis.geos import Point
>>> Point(75.778885, 26.922070) # Latitude=26.922070 & longitude=75.778885
<Point object at 0x11a282c70>
>>> # save into database
>>> Location.objects.create(coordinate=Point(75.778885, 26.922070))
<Location: Location object (1)>
Let's see how it’s been stored in the database.
>>> Location.objects.last().coordinate.coords
(75.778885, 26.92207)
Looks good. That's what we entered.
Let's do the same exercise using the Django model form. Create a forms.py file as below
from django.contrib.gis import forms
# Note: forms is being imported from gis module instead of
# `from django import forms`
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = ('coordinate',)
Next, let’s pass this data to the form and observe its response.
>>> data = {'coordinate': '75.778885, 26.92207'}
>>> form = LocationForm(data=data)
>>> form
<LocationForm bound=True, valid=Unknown, fields=(coordinate)>
>>> form.is_valid()
Error creating geometry from value '75.778885, 26.92207' (String input unrecognized as WKT EWKT, and HEXEWKB.)
Oops! We got the below error
Error creating geometry from value ‘75.778885, 26.92207’ (String input unrecognized as WKT EWKT, and HEXEWKB.)
It seems the data we provided is not in one of the acceptable formats. Here we need to provide a proper geometry type with the data.
>>> data = {'coordinate': 'POINT(75.778885 26.92207)'}
>>> form = LocationForm(data=data)
>>> form.is_valid()
True
Nice, It worked! But wait… Did it really? Too soon to celebrate 😏. Let’s save this form and verify the data in the database.
>>> form.save()
<Location: Location object (2)
>>> Location.objects.last().coordinate.coords
(0.0006807333060903553, 0.0002418450696118364)
Whaaaaaaat?
This is not what we provided.
What went wrong? Seems like Django forms require the srid
value explicitly. Let's tweak the data and go through the same steps again.
>>> data = {'coordinate': 'SRID=4326;POINT(75.778885 26.92207)'}
>>> form = LocationForm(data=data)
>>> form.is_valid()
True
>>> form.save()
<Location: Location object (3)>
Verify the database.
>>> Location.objects.last().coordinate.coords
(75.778885, 26.92207)
>>>
Fantastic! At last, our inserted data is visible. Victory ✌️!
The question, now, is where and how should we implement this change in the codebase.
We’ve got a couple of options on the table here:
- Tweaking the payload before feeding it into the form. Not the best spot for it, especially if we’re using this form across multiple sections. That brings us to the second choice.
- Overriding the __init__ method within the Form class, consolidating all the logic into one central place.
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = ('coordinate',)
def __init__(self, *args, **kwargs):
coordinate = kwargs['data'].pop('coordinate', None)
if coordinate:
# remove comma, as we need single space between two numbers for Point().
coordinate = coordinate.replace(',', '')
kwargs['data']['coordinate'] = f'SRID=4326;POINT({coordinate})' super(LocationForm, self).__init__(*args, **kwargs)
Now we don’t need to pass Geometry Type(GEOM_TYPE
) and SRID
in the data. we can simply pass the raw point data as we did in the very first step.
>>> data = {'coordinate': '75.778885, 26.92207'}
>>> form = LocationForm(data=data)
>>> form.save()
<Location: Location object (4)>
Verify the database.
>>> Location.objects.last().coordinate.coords
(75.778885, 26.92207)
>>>
👏👏👏 Sweet!
Additionally, for extra conditional checks aligned with your business logic, consider overriding the clean_<field_name> or/and clean method(s). Place all the logic there and trigger relevant exceptions or validation errors as required. Also, if your model contains multiple Point fields, consider creating a method within the class to reuse within the __init__ method to follow DRY.
For more such articles, follow me at: https://gauravvjn.medium.com.