diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..60611a15 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + # Remove unused imports/variables + - repo: https://github.com/myint/autoflake + rev: v2.2.0 + hooks: + - id: autoflake + args: + - "--in-place" + - "--remove-all-unused-imports" + - "--remove-unused-variables" + + - repo: https://github.com/pycqa/flake8 + rev: "6.1.0" + hooks: + - id: flake8 + exclude: | + ./tests/.*settings.*.py + docs/.* + args: + - '--max-line-length=110' + - '--ignore=W605,W503,W504' + + - repo: https://github.com/pycqa/isort + rev: "5.12.0" + hooks: + - id: isort + args: + - 'multi_line_output=3' + - 'use_parentheses=True' + - 'include_trailing_comma=True' + - 'force_grid_wrap=0' + - 'line_length=88' \ No newline at end of file diff --git a/README.rst b/README.rst index 66b45310..7f8a32eb 100644 --- a/README.rst +++ b/README.rst @@ -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" ################################################## diff --git a/rest_framework_gis/fields.py b/rest_framework_gis/fields.py index 395ef508..d22a7e78 100644 --- a/rest_framework_gis/fields.py +++ b/rest_framework_gis/fields.py @@ -65,7 +65,7 @@ def to_internal_value(self, value): value = json.dumps(value) try: return GEOSGeometry(value) - except (GEOSException): + except GEOSException: raise ValidationError( _( 'Invalid format: string or unicode input unrecognized as GeoJSON, WKT EWKT or HEXEWKB.' diff --git a/rest_framework_gis/serializers.py b/rest_framework_gis/serializers.py deleted file mode 100644 index 60ac9384..00000000 --- a/rest_framework_gis/serializers.py +++ /dev/null @@ -1,223 +0,0 @@ -from collections import OrderedDict - -from django.contrib.gis.geos import Polygon -from django.core.exceptions import ImproperlyConfigured -from rest_framework.serializers import ( - LIST_SERIALIZER_KWARGS, - ListSerializer, - ModelSerializer, -) - -from .fields import GeometryField, GeometrySerializerMethodField # noqa - - -class GeoModelSerializer(ModelSerializer): - """ - Deprecated, will be removed in django-rest-framework-gis 1.0 - """ - - -class GeoFeatureModelListSerializer(ListSerializer): - @property - def data(self): - return super(ListSerializer, self).data - - def to_representation(self, data): - """ - Add GeoJSON compatible formatting to a serialized queryset list - """ - return OrderedDict( - ( - ("type", "FeatureCollection"), - ("features", super().to_representation(data)), - ) - ) - - -class GeoFeatureModelSerializer(ModelSerializer): - """ - A subclass of ModelSerializer - that outputs geojson-ready data as - features and feature collections - """ - - @classmethod - def many_init(cls, *args, **kwargs): - child_serializer = cls(*args, **kwargs) - list_kwargs = {'child': child_serializer} - list_kwargs.update( - { - key: value - for key, value in kwargs.items() - if key in LIST_SERIALIZER_KWARGS - } - ) - meta = getattr(cls, 'Meta', None) - list_serializer_class = getattr( - meta, 'list_serializer_class', GeoFeatureModelListSerializer - ) - return list_serializer_class(*args, **list_kwargs) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - meta = getattr(self, 'Meta') - default_id_field = None - primary_key = self.Meta.model._meta.pk.name - # use primary key as id_field when possible - if ( - not hasattr(meta, 'fields') - or meta.fields == '__all__' - or primary_key in meta.fields - ): - 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'.") - - def check_excludes(field_name, field_role): - """make sure the field is not excluded""" - if hasattr(meta, 'exclude') and field_name in meta.exclude: - raise ImproperlyConfigured( - "You cannot exclude your '{0}'.".format(field_role) - ) - - def add_to_fields(field_name): - """Make sure the field is included in the fields""" - if hasattr(meta, 'fields') and meta.fields != '__all__': - if field_name not in meta.fields: - if type(meta.fields) is tuple: - additional_fields = (field_name,) - else: - additional_fields = [field_name] - meta.fields += additional_fields - - check_excludes(meta.geo_field, 'geo_field') - add_to_fields(meta.geo_field) - - meta.bbox_geo_field = getattr(meta, 'bbox_geo_field', None) - if meta.bbox_geo_field: - check_excludes(meta.bbox_geo_field, 'bbox_geo_field') - add_to_fields(meta.bbox_geo_field) - - meta.auto_bbox = getattr(meta, 'auto_bbox', False) - if meta.bbox_geo_field and meta.auto_bbox: - raise ImproperlyConfigured( - "You must eiher define a 'bbox_geo_field' or " - "'auto_bbox', but you can not set both" - ) - - def to_representation(self, instance): - """ - Serialize objects -> primitives. - """ - # prepare OrderedDict geojson structure - feature = OrderedDict() - - # keep track of the fields being processed - processed_fields = set() - - # optional id attribute - if self.Meta.id_field: - field = self.fields[self.Meta.id_field] - value = field.get_attribute(instance) - feature["id"] = field.to_representation(value) - processed_fields.add(self.Meta.id_field) - - # required type attribute - # 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) - - # Bounding Box - # if auto_bbox feature is enabled - # bbox will be determined automatically automatically - if self.Meta.auto_bbox and geo_value: - feature["bbox"] = geo_value.extent - # otherwise it can be determined via another field - elif self.Meta.bbox_geo_field: - field = self.fields[self.Meta.bbox_geo_field] - value = field.get_attribute(instance) - feature["bbox"] = value.extent if hasattr(value, 'extent') else None - processed_fields.add(self.Meta.bbox_geo_field) - - # the list of fields that will be processed by get_properties - # we will remove fields that have been already processed - # to increase performance on large numbers - fields = [ - field_value - for field_key, field_value in self.fields.items() - if field_key not in processed_fields - ] - - # GeoJSON properties - feature["properties"] = self.get_properties(instance, fields) - - return feature - - def get_properties(self, instance, fields): - """ - Get the feature metadata which will be used for the GeoJSON - "properties" key. - - By default it returns all serializer fields excluding those used for - the ID, the geometry and the bounding box. - - :param instance: The current Django model instance - :param fields: The list of fields to process (fields already processed have been removed) - :return: OrderedDict containing the properties of the current feature - :rtype: OrderedDict - """ - properties = OrderedDict() - - for field in fields: - if field.write_only: - continue - value = field.get_attribute(instance) - representation = None - if value is not None: - representation = field.to_representation(value) - properties[field.field_name] = representation - - return properties - - def to_internal_value(self, data): - """ - Override the parent method to first remove the GeoJSON formatting - """ - if 'properties' in data: - data = self.unformat_geojson(data) - return super().to_internal_value(data) - - def unformat_geojson(self, feature): - """ - This function should return a dictionary containing keys which maps - to serializer fields. - - Remember that GeoJSON contains a key "properties" which contains the - feature metadata. This should be flattened to make sure this - metadata is stored in the right serializer fields. - - :param feature: The dictionary containing the feature data directly - from the GeoJSON data. - :return: A new dictionary which maps the GeoJSON values to - serializer fields - """ - attrs = feature["properties"] - - if 'geometry' in feature: - attrs[self.Meta.geo_field] = feature['geometry'] - - if self.Meta.id_field and 'id' in feature: - attrs[self.Meta.id_field] = feature['id'] - - if self.Meta.bbox_geo_field and 'bbox' in feature: - attrs[self.Meta.bbox_geo_field] = Polygon.from_bbox(feature['bbox']) - - return attrs diff --git a/tests/django_restframework_gis_tests/migrations/0001_initial.py b/tests/django_restframework_gis_tests/migrations/0001_initial.py index 7458e470..f33b4b0c 100644 --- a/tests/django_restframework_gis_tests/migrations/0001_initial.py +++ b/tests/django_restframework_gis_tests/migrations/0001_initial.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/tests/django_restframework_gis_tests/migrations/0002_nullable.py b/tests/django_restframework_gis_tests/migrations/0002_nullable.py index 5f0d7530..06059ef5 100644 --- a/tests/django_restframework_gis_tests/migrations/0002_nullable.py +++ b/tests/django_restframework_gis_tests/migrations/0002_nullable.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ('django_restframework_gis_tests', '0001_initial'), ] diff --git a/tests/django_restframework_gis_tests/models.py b/tests/django_restframework_gis_tests/models.py index 3c8713d0..cf6adf4a 100644 --- a/tests/django_restframework_gis_tests/models.py +++ b/tests/django_restframework_gis_tests/models.py @@ -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 @@ -45,14 +44,14 @@ def save(self, *args, **kwargs): class Location(BaseModel): - pass + geometry = models.GeometryField() -class LocatedFile(BaseModel): +class LocatedFile(Location): file = models.FileField(upload_to='located_files', blank=True, null=True) -class BoxedLocation(BaseModel): +class BoxedLocation(Location): bbox_geometry = models.PolygonField() diff --git a/tests/django_restframework_gis_tests/serializers.py b/tests/django_restframework_gis_tests/serializers.py index 95ef3371..9c408216 100644 --- a/tests/django_restframework_gis_tests/serializers.py +++ b/tests/django_restframework_gis_tests/serializers.py @@ -185,6 +185,13 @@ class Meta: fields = ['name', 'slug', 'id'] +class NoGeoFeatureMethodSerializer(gis_serializers.GeoFeatureModelSerializer): + class Meta: + model = Location + geo_field = None + fields = ['name', 'slug', 'id'] + + class PointSerializer(gis_serializers.GeoFeatureModelSerializer): class Meta: model = PointModel diff --git a/tests/django_restframework_gis_tests/tests.py b/tests/django_restframework_gis_tests/tests.py index d4305800..013abb70 100644 --- a/tests/django_restframework_gis_tests/tests.py +++ b/tests/django_restframework_gis_tests/tests.py @@ -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): + location = Location.objects.create(name='No geometry value') + location_loaded = Location.objects.get(pk=location.id) + self.assertEqual(location_loaded.name, "No geometry value") + url = reverse('api_geojson_location_details_nogeo', args=[location.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() diff --git a/tests/django_restframework_gis_tests/views.py b/tests/django_restframework_gis_tests/views.py index bd290338..1a289e25 100644 --- a/tests/django_restframework_gis_tests/views.py +++ b/tests/django_restframework_gis_tests/views.py @@ -23,6 +23,7 @@ LocationGeoFeatureSlugSerializer, LocationGeoFeatureWritableIdSerializer, LocationGeoSerializer, + NoGeoFeatureMethodSerializer, NoneGeoFeatureMethodSerializer, PaginatedLocationGeoSerializer, PolygonModelSerializer, @@ -167,6 +168,15 @@ class GeojsonLocationDetailsNone(generics.RetrieveUpdateDestroyAPIView): geojson_location_details_none = GeojsonLocationDetailsNone.as_view() +class GeojsonLocationDetailsNoGeo(generics.RetrieveUpdateDestroyAPIView): + model = Location + serializer_class = NoGeoFeatureMethodSerializer + queryset = Location.objects.all() + + +geojson_location_details_nogeo = GeojsonLocationDetailsNoGeo.as_view() + + class GeojsonLocationSlugDetails(generics.RetrieveUpdateDestroyAPIView): model = Location lookup_field = 'slug' diff --git a/tests/settings.py b/tests/settings.py index e4e05d1c..8751b92d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -81,6 +81,6 @@ # local settings must be imported before test runner otherwise they'll be ignored try: - from local_settings import * + pass except ImportError: pass