Skip to content

Commit d9298a5

Browse files
committed
[feature] Allow geometry-less models #282
fixes #282
1 parent 5840e4f commit d9298a5

File tree

8 files changed

+94
-15
lines changed

8 files changed

+94
-15
lines changed

README.rst

+3
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ to be serialized as the "geometry". For example:
250250
# as with a ModelSerializer.
251251
fields = ('id', 'address', 'city', 'state')
252252
253+
If your model is geometry-less, you can set ``geo_field`` to ``None``
254+
and a null geometry will be produced.
255+
253256
Using GeometrySerializerMethodField as "geo_field"
254257
##################################################
255258

rest_framework_gis/serializers.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ def __init__(self, *args, **kwargs):
7272
default_id_field = primary_key
7373
meta.id_field = getattr(meta, 'id_field', default_id_field)
7474

75-
if not hasattr(meta, 'geo_field') or not meta.geo_field:
76-
raise ImproperlyConfigured("You must define a 'geo_field'.")
75+
if not hasattr(meta, 'geo_field'):
76+
raise ImproperlyConfigured(
77+
"You must define a 'geo_field'. "
78+
"Set it to None if there is no geometry."
79+
)
7780

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

9598
check_excludes(meta.geo_field, 'geo_field')
96-
add_to_fields(meta.geo_field)
99+
100+
if meta.geo_field is not None:
101+
add_to_fields(meta.geo_field)
97102

98103
meta.bbox_geo_field = getattr(meta, 'bbox_geo_field', None)
99104
if meta.bbox_geo_field:
@@ -118,7 +123,7 @@ def to_representation(self, instance):
118123
processed_fields = set()
119124

120125
# optional id attribute
121-
if self.Meta.id_field:
126+
if self.Meta.id_field is not None:
122127
field = self.fields[self.Meta.id_field]
123128
value = field.get_attribute(instance)
124129
feature["id"] = field.to_representation(value)
@@ -128,12 +133,15 @@ def to_representation(self, instance):
128133
# must be "Feature" according to GeoJSON spec
129134
feature["type"] = "Feature"
130135

131-
# required geometry attribute
132-
# MUST be present in output according to GeoJSON spec
133-
field = self.fields[self.Meta.geo_field]
134-
geo_value = field.get_attribute(instance)
135-
feature["geometry"] = field.to_representation(geo_value)
136-
processed_fields.add(self.Meta.geo_field)
136+
# geometry attribute
137+
# must be present in output according to GeoJSON spec
138+
if self.Meta.geo_field is not None:
139+
field = self.fields[self.Meta.geo_field]
140+
geo_value = field.get_attribute(instance)
141+
feature["geometry"] = field.to_representation(geo_value)
142+
processed_fields.add(self.Meta.geo_field)
143+
else:
144+
feature["geometry"] = None
137145

138146
# Bounding Box
139147
# if auto_bbox feature is enabled
@@ -211,7 +219,7 @@ def unformat_geojson(self, feature):
211219
"""
212220
attrs = feature["properties"]
213221

214-
if 'geometry' in feature:
222+
if 'geometry' in feature and self.Meta.geo_field:
215223
attrs[self.Meta.geo_field] = feature['geometry']
216224

217225
if self.Meta.id_field and 'id' in feature:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 3.2.24 on 2024-02-28 22:57
2+
3+
import django.contrib.gis.db.models.fields
4+
from django.db import migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('django_restframework_gis_tests', '0003_schema_models'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='boxedlocation',
16+
name='geometry',
17+
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
18+
),
19+
migrations.AlterField(
20+
model_name='locatedfile',
21+
name='geometry',
22+
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
23+
),
24+
migrations.AlterField(
25+
model_name='location',
26+
name='geometry',
27+
field=django.contrib.gis.db.models.fields.GeometryField(srid=4326),
28+
),
29+
]

tests/django_restframework_gis_tests/models.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class BaseModel(models.Model):
2020
name = models.CharField(max_length=32)
2121
slug = models.SlugField(max_length=128, unique=True, blank=True)
2222
timestamp = models.DateTimeField(null=True, blank=True)
23-
geometry = models.GeometryField()
2423

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

4645

47-
class Location(BaseModel):
46+
class BaseModelGeometry(BaseModel):
47+
class Meta:
48+
abstract = True
49+
50+
geometry = models.GeometryField()
51+
52+
53+
class Location(BaseModelGeometry):
4854
pass
4955

5056

51-
class LocatedFile(BaseModel):
57+
class LocatedFile(BaseModelGeometry):
5258
file = models.FileField(upload_to='located_files', blank=True, null=True)
5359

5460

55-
class BoxedLocation(BaseModel):
61+
class BoxedLocation(BaseModelGeometry):
5662
bbox_geometry = models.PolygonField()
5763

5864

tests/django_restframework_gis_tests/serializers.py

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
MultiLineStringModel,
1313
MultiPointModel,
1414
MultiPolygonModel,
15+
Nullable,
1516
PointModel,
1617
PolygonModel,
1718
)
@@ -185,6 +186,13 @@ class Meta:
185186
fields = ['name', 'slug', 'id']
186187

187188

189+
class NoGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer):
190+
class Meta:
191+
model = Nullable
192+
geo_field = None
193+
fields = ['name', 'slug', 'id']
194+
195+
188196
class PointSerializer(gis_serializers.GeoFeatureModelSerializer):
189197
class Meta:
190198
model = PointModel

tests/django_restframework_gis_tests/tests.py

+10
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,16 @@ def test_geometry_serializer_method_field_none(self):
634634
self.assertEqual(response.data['properties']['name'], 'None value')
635635
self.assertEqual(response.data['geometry'], None)
636636

637+
def test_geometry_serializer_method_field_nogeo(self):
638+
nullable = Nullable.objects.create(name='No geometry value')
639+
nullable_loaded = Nullable.objects.get(pk=nullable.id)
640+
self.assertEqual(nullable_loaded.name, "No geometry value")
641+
url = reverse('api_geojson_nullable_details_nogeo', args=[nullable.id])
642+
response = self.client.generic('GET', url, content_type='application/json')
643+
self.assertEqual(response.status_code, 200)
644+
self.assertEqual(response.data['properties']['name'], 'No geometry value')
645+
self.assertEqual(response.data['geometry'], None)
646+
637647
def test_nullable_empty_geometry(self):
638648
empty = Nullable(name='empty', geometry='POINT EMPTY')
639649
empty.full_clean()

tests/django_restframework_gis_tests/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
views.geojson_nullable_details,
2323
name='api_geojson_nullable_details',
2424
),
25+
path(
26+
'geojson_nogeo/<int:pk>/',
27+
views.geojson_nullable_details_nogeo,
28+
name='api_geojson_nullable_details_nogeo',
29+
),
2530
path(
2631
'geojson_hidden/<int:pk>/',
2732
views.geojson_location_details_hidden,

tests/django_restframework_gis_tests/views.py

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
LocationGeoFeatureSlugSerializer,
2424
LocationGeoFeatureWritableIdSerializer,
2525
LocationGeoSerializer,
26+
NoGeoFeatureMethodSerializer,
2627
NoneGeoFeatureMethodSerializer,
2728
PaginatedLocationGeoSerializer,
2829
PolygonModelSerializer,
@@ -167,6 +168,15 @@ class GeojsonLocationDetailsNone(generics.RetrieveUpdateDestroyAPIView):
167168
geojson_location_details_none = GeojsonLocationDetailsNone.as_view()
168169

169170

171+
class GeojsonNullableDetailsNoGeo(generics.RetrieveUpdateDestroyAPIView):
172+
model = Nullable
173+
serializer_class = NoGeoFeatureMethodSerializer
174+
queryset = Nullable.objects.all()
175+
176+
177+
geojson_nullable_details_nogeo = GeojsonNullableDetailsNoGeo.as_view()
178+
179+
170180
class GeojsonLocationSlugDetails(generics.RetrieveUpdateDestroyAPIView):
171181
model = Location
172182
lookup_field = 'slug'

0 commit comments

Comments
 (0)