Migration Guide: Converting Legacy map_fields.PointField
to Django GIS PointField
This documentation explains how to migrate from the custom and legacy point field (map_fields.PointField
) to Django’s standard GeoDjango PointField
, based on the latest changes in the branch and the example from PR #6023.
So far we have migrated the projects app, for which migration field and data is happening in the adhocracy4. See relevant migrations 0048 and 0049.
Adhocracy+ apps that need to migrate in the future: - mapideas - budgeting
1. Development how-to
In the following example, we outline the steps required for changing the mapideas module.
Previously, the project used a custom PointField
from adhocracy4.maps.fields
:
# Before (legacy field)
from adhocracy4.maps import fields as map_fields
class AbstractMapIdea(models.Model):
point = map_fields.PointField()
This field will be now removed in favor of Django’s built-in GIS field:
from django.contrib.gis.db import models as gis_models
class AbstractMapIdea(models.Model):
coordinates = gis_models.PointField(null=True, blank=True)
2. Migration Steps
Step 1: Add the New PointField
- Add the new GeoDjango
PointField
(coordinates
) to your model. - Set
null=True
andblank=True
to allow a uninterrupted migration and make the field optional.
from django.contrib.gis.db import models as gis_models
class AbstractMapIdea(models.Model):
coordinates = gis_models.PointField(null=True, blank=True)
street_name = models.CharField(null=True, blank=True, max_length=200)
house_number = models.CharField(null=True, blank=True, max_length=10)
zip_code = models.CharField(null=True, blank=True, max_length=20)
- Keep the old
point = map_fields.PointField()
field temporarily during migration.
Step 2: Create and Apply Schema Migration
- Run migrations to add the new field
coordnites
:
python manage.py makemigrations
python manage.py migrate
Step 3: Data Migration to Populate New Field
- Create a data migration to copy data from the old
point
field to the newcoordinates
field.
Example migration snippet:
import json
import logging
from django.contrib.gis.geos import GEOSGeometry
from django.db import migrations
from django.contrib.gis.geos import Point
logger = logging.getLogger(__name__)
def migrate_point_to_coordinates(apps, schema_editor):
MapIdea = apps.get_model('a4_candy_mapideas', 'MapIdea')
for idea in MapIdea.objects.exclude(point__isnull=True):
try:
# Handle case where point is already parsed (dict) or still a string
point_data = idea.point
if isinstance(point_data, str):
point_data = json.loads(point_data)
point_data = dict(point_data)
# Extract geometry and properties
geometry = point_data.get("geometry", {})
properties = point_data.get("properties", {})
if not geometry:
logger.warning(
"error migrating point of idea " + idea.name + ": " + str(point_data)
)
continue
# Create GEOSGeometry from coordinates
if geometry.get("type") == "Point" and "coordinates" in geometry:
geojson = {"type": "Point", "coordinates": geometry["coordinates"]}
point = GEOSGeometry(json.dumps(geojson), srid=4326)
# Update all fields
MapIdea.objects.filter(id=idea.id).update(
coordinates=point,
street_name=properties.get("strname", ""),
house_number=properties.get("hsnr", ""),
zip_code=properties.get("plz", ""),
)
except (ValueError, TypeError, KeyError, json.JSONDecodeError) as e:
logger.warning(f"Skipping {project.id} {project.name}: {str(e)}")
class Migration(migrations.Migration):
dependencies = [
('a4_candy_mapideas', 'previous_migration'),
]
operations = [
migrations.RunPython(migrate_point_to_coordinates),
]
- Run the migration:
python manage.py migrate
Step 4: Remove the Old map_fields.PointField
-
After verifying the new field is populated and working correctly, remove the old
point
field from the model. -
Create and apply a migration to drop the old field with
python manage.py makemigrations
andpython manage.py migrate
Step 5: Add new PointField
- If all data is migrated, add a new Geo GIS field named point and copy data from coordinates. We need to do this because a simple renaming a GeoDjango PointField (or any GIS field) directly via migrations often fails due to spatial database complexities and Django's migration detection limitations. E.g: GIS fields like PointField require special database metadata (e.g., PostGIS geometry_columns table). A simple column rename won’t update these registries, leading to broken spatial queries.
# Before
coordinates = gis_models.PointField(null=True, blank=True)
# After (add a new `point` field)
coordinates = gis_models.PointField(null=True, blank=True)
point = gis_models.PointField(null=True, blank=True)
- Create and apply the migration to add the field running the python and migration commands as above.
Step 6: Copy data from coordinates
to New PointField with a custom Migration:
from django.db import migrations
def migrate_geos_point_field(apps, schema_editor):
MapIdea = apps.get_model("a4_candy_mapideas", "MapIdea")
for idea in MapIdea.objects.exclude(coordinates__isnull=True)():
idea.point = idea.coordinates
idea.save()
class Migration(migrations.Migration):
dependencies = [
("a4_candy_mapideas", "previous_numbered_migration"),
]
operations = [
migrations.RunPython(
migrate_geos_point_field, reverse_code=migrations.RunPython.noop
),
]
- Run the migration:
python manage.py migrate
Step 7: Remove the coordinates PointField
-
After verifying the new field
point
is populated and working correctly, remove thecoordinates
field from the model. -
Create and apply a migration to drop the coordinates field.
The final version of the model will look like this: ```python
class AbstractMapIdea(models.Model): point = gis_models.PointField(null=True, blank=True) street_name = models.CharField(null=True, blank=True, max_length=200) house_number = models.CharField(null=True, blank=True, max_length=10) zip_code = models.CharField(null=True, blank=True, max_length=20)
3. Important Notes
- Coordinate Order: GeoDjango expects
Point(longitude, latitude)
. - Default Values: When setting defaults for
PointField
, use aPoint
instance, not a tuple or list. - Database Setup: Ensure PostGIS extension is enabled in your postgres database or use spatialite3 if working with sqlite.
- Indexing: Consider adding a spatial index on the new field for efficient spatial queries.
4. Summary Table
Step | Action | Notes |
---|---|---|
1 | Add new coordinates PointField |
Keep old point field temporarily |
2 | Apply schema migration | Adds new GIS field to DB |
3 | Data migration to copy data | Run the django command migrate |
4 | Remove old point field |
After data verification |
5 | Add new PointField point |
Run makemigrations and migrate |
6 | Copy data from coordinates to point |
With a custom migration |
7 | Remove coordinates field |
CRun makemigrations and migrate |