Skip to content

Commit 74efdc3

Browse files
committed
[feature] Allow geometry-less models #282
fixes #282
1 parent f743b1c commit 74efdc3

File tree

7 files changed

+130
-13
lines changed

7 files changed

+130
-13
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

+15-9
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"""
@@ -128,12 +131,15 @@ def to_representation(self, instance):
128131
# must be "Feature" according to GeoJSON spec
129132
feature["type"] = "Feature"
130133

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)
134+
# geometry attribute
135+
# must be present in output according to GeoJSON spec
136+
if self.Meta.geo_field:
137+
field = self.fields[self.Meta.geo_field]
138+
geo_value = field.get_attribute(instance)
139+
feature["geometry"] = field.to_representation(geo_value)
140+
processed_fields.add(self.Meta.geo_field)
141+
else:
142+
feature["geometry"] = None
137143

138144
# Bounding Box
139145
# if auto_bbox feature is enabled
@@ -211,7 +217,7 @@ def unformat_geojson(self, feature):
211217
"""
212218
attrs = feature["properties"]
213219

214-
if 'geometry' in feature:
220+
if 'geometry' in feature and self.Meta.geo_field:
215221
attrs[self.Meta.geo_field] = feature['geometry']
216222

217223
if self.Meta.id_field and 'id' in feature:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Generated by Django 4.2.6 on 2023-10-17 12:24
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("django_restframework_gis_tests", "0003_schema_models"),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name="boxedlocation",
16+
name="geometry",
17+
),
18+
migrations.RemoveField(
19+
model_name="boxedlocation",
20+
name="id",
21+
),
22+
migrations.RemoveField(
23+
model_name="boxedlocation",
24+
name="name",
25+
),
26+
migrations.RemoveField(
27+
model_name="boxedlocation",
28+
name="slug",
29+
),
30+
migrations.RemoveField(
31+
model_name="boxedlocation",
32+
name="timestamp",
33+
),
34+
migrations.RemoveField(
35+
model_name="locatedfile",
36+
name="geometry",
37+
),
38+
migrations.RemoveField(
39+
model_name="locatedfile",
40+
name="id",
41+
),
42+
migrations.RemoveField(
43+
model_name="locatedfile",
44+
name="name",
45+
),
46+
migrations.RemoveField(
47+
model_name="locatedfile",
48+
name="slug",
49+
),
50+
migrations.RemoveField(
51+
model_name="locatedfile",
52+
name="timestamp",
53+
),
54+
migrations.AddField(
55+
model_name="boxedlocation",
56+
name="location_ptr",
57+
field=models.OneToOneField(
58+
auto_created=True,
59+
default=None,
60+
on_delete=django.db.models.deletion.CASCADE,
61+
parent_link=True,
62+
primary_key=True,
63+
serialize=False,
64+
to="django_restframework_gis_tests.location",
65+
),
66+
preserve_default=False,
67+
),
68+
migrations.AddField(
69+
model_name="locatedfile",
70+
name="location_ptr",
71+
field=models.OneToOneField(
72+
auto_created=True,
73+
default=None,
74+
on_delete=django.db.models.deletion.CASCADE,
75+
parent_link=True,
76+
primary_key=True,
77+
serialize=False,
78+
to="django_restframework_gis_tests.location",
79+
),
80+
preserve_default=False,
81+
),
82+
]

tests/django_restframework_gis_tests/models.py

+3-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
@@ -45,14 +44,14 @@ def save(self, *args, **kwargs):
4544

4645

4746
class Location(BaseModel):
48-
pass
47+
geometry = models.GeometryField()
4948

5049

51-
class LocatedFile(BaseModel):
50+
class LocatedFile(Location):
5251
file = models.FileField(upload_to='located_files', blank=True, null=True)
5352

5453

55-
class BoxedLocation(BaseModel):
54+
class BoxedLocation(Location):
5655
bbox_geometry = models.PolygonField()
5756

5857

tests/django_restframework_gis_tests/serializers.py

+7
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ class Meta:
185185
fields = ['name', 'slug', 'id']
186186

187187

188+
class NoGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer):
189+
class Meta:
190+
model = Location
191+
geo_field = None
192+
fields = ['name', 'slug', 'id']
193+
194+
188195
class PointSerializer(gis_serializers.GeoFeatureModelSerializer):
189196
class Meta:
190197
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+
location = Location.objects.create(name='No geometry value')
639+
location_loaded = Location.objects.get(pk=location.id)
640+
self.assertEqual(location_loaded.name, "No geometry value")
641+
url = reverse('api_geojson_location_details_nogeo', args=[location.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/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 GeojsonLocationDetailsNoGeo(generics.RetrieveUpdateDestroyAPIView):
172+
model = Location
173+
serializer_class = NoGeoFeatureMethodSerializer
174+
queryset = Location.objects.all()
175+
176+
177+
geojson_location_details_nogeo = GeojsonLocationDetailsNoGeo.as_view()
178+
179+
170180
class GeojsonLocationSlugDetails(generics.RetrieveUpdateDestroyAPIView):
171181
model = Location
172182
lookup_field = 'slug'

0 commit comments

Comments
 (0)