-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
maps: add custom PointSerializerMixin which allows to save geojson
points as GeoDjango to the database for better filtering capabilites and serializes them as geojson features. - add PointInPolygon validator which validates that a given point is in the provided polygon.
- Loading branch information
Showing
11 changed files
with
443 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import json | ||
from collections import OrderedDict | ||
|
||
|
||
class PointSerializerMixin: | ||
"""Serializes a GeoDjango Point field into a geojson feature. Requires the field `geo_field` | ||
on the Meta class of the serializer to be set to the name of the model field containing the point. | ||
""" | ||
|
||
def to_internal_value(self, data): | ||
if self.Meta.geo_field and self.Meta.geo_field in data: | ||
geo_field = data[self.Meta.geo_field] | ||
point = json.loads(geo_field) | ||
data = data.copy() | ||
if "geometry" in point: | ||
data[self.Meta.geo_field] = json.dumps(point["geometry"]) | ||
properties = self.get_properties() | ||
if "properties" in point: | ||
for property in properties: | ||
if property in point["properties"]: | ||
data[property] = point["properties"][property] | ||
return super().to_internal_value(data) | ||
|
||
def to_representation(self, instance): | ||
data = super().to_representation(instance) | ||
if self.Meta.geo_field and self.Meta.geo_field in data: | ||
feature = OrderedDict() | ||
feature["type"] = "Feature" | ||
feature["geometry"] = data[self.Meta.geo_field] | ||
props = OrderedDict() | ||
properties = self.get_properties() | ||
for property in properties: | ||
if hasattr(instance, property): | ||
props[property] = getattr(instance, property) | ||
feature["properties"] = props | ||
data[self.Meta.geo_field] = feature | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from django.contrib.gis.geos import Polygon | ||
from django.core.exceptions import ValidationError | ||
from django.utils.deconstruct import deconstructible | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
@deconstructible | ||
class PointInPolygonValidator: | ||
"""Validate that the given point is within the polygon, otherwise raise ValidationError.""" | ||
|
||
polygon: Polygon = None | ||
message = _("Point is not inside the specified area") | ||
code = "invalid" | ||
|
||
def __init__(self, polygon): | ||
self.polygon = polygon | ||
|
||
def __call__(self, value): | ||
"""""" | ||
if not self.polygon.contains(value): | ||
raise ValidationError(message=self.message, code=self.code) | ||
|
||
def __eq__(self, other): | ||
return ( | ||
isinstance(other, PointInPolygonValidator) | ||
and self.message == other.message | ||
and self.code == other.code | ||
and self.polygon == other.polygon | ||
) |
40 changes: 40 additions & 0 deletions
40
adhocracy4/projects/migrations/0048_project_geos_point_project_house_number_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Generated by Django 4.2.17 on 2025-01-28 14:21 | ||
|
||
import django.contrib.gis.db.models.fields | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("a4projects", "0047_alter_project_image_alter_project_tile_image"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="project", | ||
name="geos_point", | ||
field=django.contrib.gis.db.models.fields.PointField( | ||
blank=True, | ||
help_text="Locate your project. Click inside the marked area or type in an address to set the marker. A set marker can be dragged when pressed.", | ||
null=True, | ||
srid=4326, | ||
verbose_name="Can your project be located on the map?", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="project", | ||
name="house_number", | ||
field=models.CharField(blank=True, max_length=10, null=True), | ||
), | ||
migrations.AddField( | ||
model_name="project", | ||
name="streetname", | ||
field=models.CharField(blank=True, max_length=200, null=True), | ||
), | ||
migrations.AddField( | ||
model_name="project", | ||
name="zip_code", | ||
field=models.CharField(blank=True, max_length=20, null=True), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Generated by Django 4.2.17 on 2025-01-27 15:11 | ||
|
||
import json | ||
import logging | ||
import django.contrib.gis.db.models.fields | ||
|
||
from django.contrib.gis.geos import GEOSGeometry | ||
from django.contrib.gis.geos import Point | ||
from django.db import migrations, models | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def migrate_project_point_field(apps, schema_editor): | ||
project = apps.get_model("a4projects", "Project") | ||
for project in project.objects.all(): | ||
geojson_point = project.point | ||
if not "geometry" in geojson_point: | ||
logger.warning( | ||
"error migrating point of project " | ||
+ project.name | ||
+ ": " | ||
+ str(geojson_point) | ||
) | ||
continue | ||
project.geos_point = GEOSGeometry(json.dumps(geojson_point["geometry"])) | ||
if "properties" in geojson_point: | ||
properties = geojson_point["properties"] | ||
if "strname" in properties: | ||
project.street_name = properties["strname"] | ||
if "hsnr" in properties: | ||
project.house_number = properties["hsnr"] | ||
if "plz" in properties: | ||
project.zip_code = properties["plz"] | ||
project.save() | ||
|
||
|
||
def migrate_project_geos_point_field(apps, schema_editor): | ||
project = apps.get_model("a4projects", "Project") | ||
for project in project.objects.all(): | ||
project.point = project.geos_point | ||
project.save() | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("a4projects", "0048_project_geos_point_project_house_number_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.RunPython( | ||
migrate_project_point_field, reverse_code=migrations.RunPython.noop | ||
), | ||
migrations.RemoveField( | ||
model_name="project", | ||
name="point", | ||
), | ||
migrations.AddField( | ||
model_name="project", | ||
name="point", | ||
field=django.contrib.gis.db.models.fields.PointField( | ||
blank=True, | ||
help_text="Locate your project. Click inside the marked area or type in an address to set the marker. A set marker can be dragged when pressed.", | ||
null=True, | ||
srid=4326, | ||
verbose_name="Can your project be located on the map?", | ||
), | ||
), | ||
migrations.RunPython( | ||
migrate_project_geos_point_field, reverse_code=migrations.RunPython.noop | ||
), | ||
migrations.RemoveField( | ||
model_name="project", | ||
name="geos_point", | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
### Added | ||
|
||
- add new `PointSerializerMixin` which enables a serializer to correctly save geojson features as GeoDjango | ||
in the database and serialize it back as geojson feature. | ||
|
||
### Changed | ||
|
||
- use spatialite as database to support GeoDjango |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import json | ||
|
||
import pytest | ||
from django.conf import settings | ||
from django.contrib.gis.geos import GEOSGeometry | ||
from django.forms import ValidationError | ||
|
||
from adhocracy4.maps.validators import PointInPolygonValidator | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_point_in_polygon_validator_valid_point(): | ||
polygon = GEOSGeometry( | ||
json.dumps(settings.BERLIN_POLYGON["features"][0]["geometry"]) | ||
) | ||
validator = PointInPolygonValidator(polygon=polygon) | ||
|
||
# point within the berlin polygon | ||
geojson_point = { | ||
"type": "Feature", | ||
"geometry": { | ||
"type": "Point", | ||
"coordinates": [13.411924777644563, 52.499598134440944], | ||
}, | ||
} | ||
point = GEOSGeometry(json.dumps(geojson_point["geometry"])) | ||
validator(point) | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_point_in_polygon_validator_invalid_point(): | ||
polygon = GEOSGeometry( | ||
json.dumps(settings.BERLIN_POLYGON["features"][0]["geometry"]) | ||
) | ||
validator = PointInPolygonValidator(polygon=polygon) | ||
|
||
# point outside the berlin polygon | ||
geojson_point = { | ||
"type": "Feature", | ||
"geometry": {"type": "Point", "coordinates": [13.459894, 51.574425]}, | ||
} | ||
point = GEOSGeometry(json.dumps(geojson_point["geometry"])) | ||
with pytest.raises(ValidationError): | ||
validator(point) |
Oops, something went wrong.