Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow geometry-less models #285

Merged
merged 1 commit into from Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.rst
Expand Up @@ -250,6 +250,9 @@ to be serialized as the "geometry". For example:
# as with a ModelSerializer.
fields = ('id', 'address', 'city', 'state')

If your model is geometry-less, you can set ``geo_field`` to ``None``
and a null geometry will be produced.

Using GeometrySerializerMethodField as "geo_field"
##################################################

Expand Down
28 changes: 18 additions & 10 deletions rest_framework_gis/serializers.py
Expand Up @@ -72,8 +72,11 @@ def __init__(self, *args, **kwargs):
default_id_field = primary_key
meta.id_field = getattr(meta, 'id_field', default_id_field)

if not hasattr(meta, 'geo_field') or not meta.geo_field:
raise ImproperlyConfigured("You must define a 'geo_field'.")
if not hasattr(meta, 'geo_field'):
raise ImproperlyConfigured(
"You must define a 'geo_field'. "
"Set it to None if there is no geometry."
)

def check_excludes(field_name, field_role):
"""make sure the field is not excluded"""
Expand All @@ -93,7 +96,9 @@ def add_to_fields(field_name):
meta.fields += additional_fields

check_excludes(meta.geo_field, 'geo_field')
add_to_fields(meta.geo_field)

if meta.geo_field is not None:
add_to_fields(meta.geo_field)

meta.bbox_geo_field = getattr(meta, 'bbox_geo_field', None)
if meta.bbox_geo_field:
Expand Down Expand Up @@ -128,12 +133,15 @@ def to_representation(self, instance):
# must be "Feature" according to GeoJSON spec
feature["type"] = "Feature"

# required geometry attribute
# MUST be present in output according to GeoJSON spec
field = self.fields[self.Meta.geo_field]
geo_value = field.get_attribute(instance)
feature["geometry"] = field.to_representation(geo_value)
processed_fields.add(self.Meta.geo_field)
# geometry attribute
# must be present in output according to GeoJSON spec
if self.Meta.geo_field:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self.Meta.geo_field:
if self.Meta.geo_field is not None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good like this, we want to ignore stuff like "" or 0 that ended up here by accident.

field = self.fields[self.Meta.geo_field]
geo_value = field.get_attribute(instance)
feature["geometry"] = field.to_representation(geo_value)
processed_fields.add(self.Meta.geo_field)
else:
feature["geometry"] = None

# Bounding Box
# if auto_bbox feature is enabled
Expand Down Expand Up @@ -211,7 +219,7 @@ def unformat_geojson(self, feature):
"""
attrs = feature["properties"]

if 'geometry' in feature:
if 'geometry' in feature and self.Meta.geo_field:
attrs[self.Meta.geo_field] = feature['geometry']

if self.Meta.id_field and 'id' in feature:
Expand Down
@@ -0,0 +1,29 @@
# Generated by Django 3.2.24 on 2024-02-28 22:57

import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('django_restframework_gis_tests', '0003_schema_models'),
]

operations = [
migrations.AlterField(
model_name='boxedlocation',
name='geometry',
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
),
migrations.AlterField(
model_name='locatedfile',
name='geometry',
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
),
migrations.AlterField(
model_name='location',
name='geometry',
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
),
]
14 changes: 10 additions & 4 deletions tests/django_restframework_gis_tests/models.py
Expand Up @@ -20,7 +20,6 @@ class BaseModel(models.Model):
name = models.CharField(max_length=32)
slug = models.SlugField(max_length=128, unique=True, blank=True)
timestamp = models.DateTimeField(null=True, blank=True)
geometry = models.GeometryField()

class Meta:
abstract = True
Expand All @@ -44,15 +43,22 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)


class Location(BaseModel):
class BaseModelGeometry(BaseModel):
class Meta:
abstract = True

geometry = models.GeometryField()


class Location(BaseModelGeometry):
pass


class LocatedFile(BaseModel):
class LocatedFile(BaseModelGeometry):
file = models.FileField(upload_to='located_files', blank=True, null=True)


class BoxedLocation(BaseModel):
class BoxedLocation(BaseModelGeometry):
bbox_geometry = models.PolygonField()


Expand Down
8 changes: 8 additions & 0 deletions tests/django_restframework_gis_tests/serializers.py
Expand Up @@ -12,6 +12,7 @@
MultiLineStringModel,
MultiPointModel,
MultiPolygonModel,
Nullable,
PointModel,
PolygonModel,
)
Expand Down Expand Up @@ -185,6 +186,13 @@ class Meta:
fields = ['name', 'slug', 'id']


class NoGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer):
class Meta:
model = Nullable
geo_field = None
fields = ['name', 'slug', 'id']


class PointSerializer(gis_serializers.GeoFeatureModelSerializer):
class Meta:
model = PointModel
Expand Down
10 changes: 10 additions & 0 deletions tests/django_restframework_gis_tests/tests.py
Expand Up @@ -634,6 +634,16 @@ def test_geometry_serializer_method_field_none(self):
self.assertEqual(response.data['properties']['name'], 'None value')
self.assertEqual(response.data['geometry'], None)

def test_geometry_serializer_method_field_nogeo(self):
nullable = Nullable.objects.create(name='No geometry value')
nullable_loaded = Nullable.objects.get(pk=nullable.id)
self.assertEqual(nullable_loaded.name, "No geometry value")
url = reverse('api_geojson_nullable_details_nogeo', args=[nullable.id])
response = self.client.generic('GET', url, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['properties']['name'], 'No geometry value')
self.assertEqual(response.data['geometry'], None)

def test_nullable_empty_geometry(self):
empty = Nullable(name='empty', geometry='POINT EMPTY')
empty.full_clean()
Expand Down
5 changes: 5 additions & 0 deletions tests/django_restframework_gis_tests/urls.py
Expand Up @@ -22,6 +22,11 @@
views.geojson_nullable_details,
name='api_geojson_nullable_details',
),
path(
'geojson_nogeo/<int:pk>/',
views.geojson_nullable_details_nogeo,
name='api_geojson_nullable_details_nogeo',
),
path(
'geojson_hidden/<int:pk>/',
views.geojson_location_details_hidden,
Expand Down
10 changes: 10 additions & 0 deletions tests/django_restframework_gis_tests/views.py
Expand Up @@ -23,6 +23,7 @@
LocationGeoFeatureSlugSerializer,
LocationGeoFeatureWritableIdSerializer,
LocationGeoSerializer,
NoGeoFeatureMethodSerializer,
NoneGeoFeatureMethodSerializer,
PaginatedLocationGeoSerializer,
PolygonModelSerializer,
Expand Down Expand Up @@ -167,6 +168,15 @@ class GeojsonLocationDetailsNone(generics.RetrieveUpdateDestroyAPIView):
geojson_location_details_none = GeojsonLocationDetailsNone.as_view()


class GeojsonNullableDetailsNoGeo(generics.RetrieveUpdateDestroyAPIView):
model = Nullable
serializer_class = NoGeoFeatureMethodSerializer
queryset = Nullable.objects.all()


geojson_nullable_details_nogeo = GeojsonNullableDetailsNoGeo.as_view()


class GeojsonLocationSlugDetails(generics.RetrieveUpdateDestroyAPIView):
model = Location
lookup_field = 'slug'
Expand Down