From 53b16bcb57d9fb753e8dcc875dd815b28934c74e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 24 May 2025 14:16:33 -0400 Subject: [PATCH 1/8] use fork for temporary testing --- .github/workflows/mongodb_settings.py | 4 +- .github/workflows/runtests.py | 153 +----------------------- .github/workflows/test-python-atlas.yml | 2 +- .github/workflows/test-python.yml | 2 +- 4 files changed, 5 insertions(+), 156 deletions(-) diff --git a/.github/workflows/mongodb_settings.py b/.github/workflows/mongodb_settings.py index 49d44a5f..28ff964f 100644 --- a/.github/workflows/mongodb_settings.py +++ b/.github/workflows/mongodb_settings.py @@ -15,13 +15,13 @@ else: DATABASES = { "default": { - "ENGINE": "django_mongodb_backend", + "ENGINE": "django_mongodb_backend_gis", "NAME": "djangotests", # Required when connecting to the Atlas image in Docker. "OPTIONS": {"directConnection": True}, }, "other": { - "ENGINE": "django_mongodb_backend", + "ENGINE": "django_mongodb_backend_gis", "NAME": "djangotests-other", "OPTIONS": {"directConnection": True}, }, diff --git a/.github/workflows/runtests.py b/.github/workflows/runtests.py index ebcc4876..4b6a6e59 100755 --- a/.github/workflows/runtests.py +++ b/.github/workflows/runtests.py @@ -4,158 +4,7 @@ import sys test_apps = [ - "admin_changelist", - "admin_checks", - "admin_custom_urls", - "admin_docs", - "admin_filters", - "admin_inlines", - "admin_ordering", - "admin_scripts", - "admin_utils", - "admin_views", - "admin_widgets", - "aggregation", - "aggregation_regress", - "annotations", - "apps", - "async", - "auth_tests", - "backends", - "basic", - "bulk_create", - "cache", - "check_framework", - "constraints", - "contenttypes_tests", - "context_processors", - "custom_columns", - "custom_lookups", - "custom_managers", - "custom_pk", - "datatypes", - "dates", - "datetimes", - "db_functions", - "defer", - "defer_regress", - "delete", - "delete_regress", - "empty", - "empty_models", - "expressions", - "expressions_case", - "field_defaults", - "file_storage", - "file_uploads", - "fixtures", - "fixtures_model_package", - "fixtures_regress", - "flatpages_tests", - "force_insert_update", - "foreign_object", - "forms_tests", - "from_db_value", - "generic_inline_admin", - "generic_relations", - "generic_relations_regress", - "generic_views", - "get_earliest_or_latest", - "get_object_or_404", - "get_or_create", - "i18n", - "indexes", - "inline_formsets", - "introspection", - "invalid_models_tests", - "known_related_objects", - "lookup", - "m2m_and_m2o", - "m2m_intermediary", - "m2m_multiple", - "m2m_recursive", - "m2m_regress", - "m2m_signals", - "m2m_through", - "m2m_through_regress", - "m2o_recursive", - "managers_regress", - "many_to_many", - "many_to_one", - "many_to_one_null", - "max_lengths", - "messages_tests", - "migrate_signals", - "migration_test_data_persistence", - "migrations", - "model_fields", - "model_forms", - "model_formsets", - "model_formsets_regress", - "model_indexes", - "model_inheritance", - "model_inheritance_regress", - "model_options", - "model_package", - "model_regress", - "model_utils", - "modeladmin", - "multiple_database", - "mutually_referential", - "nested_foreign_keys", - "null_fk", - "null_fk_ordering", - "null_queries", - "one_to_one", - "or_lookups", - "order_with_respect_to", - "ordering", - "pagination", - "prefetch_related", - "proxy_model_inheritance", - "proxy_models", - "queries", - "queryset_pickle", - "redirects_tests", - "reserved_names", - "reverse_lookup", - "save_delete_hooks", - "schema", - "select_for_update", - "select_related", - "select_related_onetoone", - "select_related_regress", - "serializers", - "servers", - "sessions_tests", - "shortcuts", - "signals", - "sitemaps_tests", - "sites_framework", - "sites_tests", - "string_lookup", - "swappable_models", - "syndication_tests", - "test_client", - "test_client_regress", - "test_runner", - "test_utils", - "timezones", - "transactions", - "unmanaged_models", - "update", - "update_only_fields", - "user_commands", - "validation", - "view_tests", - "xor_lookups", - # Add directories in django_mongodb_backend/tests - *sorted( - [ - x.name - for x in (pathlib.Path(__file__).parent.parent.parent.resolve() / "tests").iterdir() - ] - ), + "gis_tests", ] runtests = pathlib.Path(__file__).parent.resolve() / "runtests.py" run_tests_cmd = f"python3 {runtests} %s --settings mongodb_settings -v 2" diff --git a/.github/workflows/test-python-atlas.yml b/.github/workflows/test-python-atlas.yml index 175dfe18..f8d6a751 100644 --- a/.github/workflows/test-python-atlas.yml +++ b/.github/workflows/test-python-atlas.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'mongodb-forks/django' - ref: 'mongodb-5.2.x' + ref: 'mongogis' path: 'django_repo' persist-credentials: false - name: Install system packages for Django's Python test dependencies diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index d471f3f2..8cf1871c 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'mongodb-forks/django' - ref: 'mongodb-5.2.x' + ref: 'mongogis' path: 'django_repo' persist-credentials: false - name: Install system packages for Django's Python test dependencies From e26ee4f622ce54fa7c20e6a8477b851e4bd40daf Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 24 May 2025 14:13:33 -0400 Subject: [PATCH 2/8] Add GeoDjango support --- .github/workflows/test-python-atlas.yml | 2 +- .github/workflows/test-python.yml | 2 +- django_mongodb_backend_gis/__init__.py | 3 + django_mongodb_backend_gis/adapter.py | 12 ++++ django_mongodb_backend_gis/base.py | 11 +++ django_mongodb_backend_gis/features.py | 71 ++++++++++++++++++++ django_mongodb_backend_gis/functions.py | 6 ++ django_mongodb_backend_gis/operations.py | 85 ++++++++++++++++++++++++ django_mongodb_backend_gis/schema.py | 5 ++ 9 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 django_mongodb_backend_gis/__init__.py create mode 100644 django_mongodb_backend_gis/adapter.py create mode 100644 django_mongodb_backend_gis/base.py create mode 100644 django_mongodb_backend_gis/features.py create mode 100644 django_mongodb_backend_gis/functions.py create mode 100644 django_mongodb_backend_gis/operations.py create mode 100644 django_mongodb_backend_gis/schema.py diff --git a/.github/workflows/test-python-atlas.yml b/.github/workflows/test-python-atlas.yml index f8d6a751..c6727e75 100644 --- a/.github/workflows/test-python-atlas.yml +++ b/.github/workflows/test-python-atlas.yml @@ -39,7 +39,7 @@ jobs: - name: Install system packages for Django's Python test dependencies run: | sudo apt-get update - sudo apt-get install libmemcached-dev + sudo apt-get install gdal-bin libmemcached-dev - name: Install Django and its Python test dependencies run: | cd django_repo/tests/ diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 8cf1871c..2dc4663b 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -39,7 +39,7 @@ jobs: - name: Install system packages for Django's Python test dependencies run: | sudo apt-get update - sudo apt-get install libmemcached-dev + sudo apt-get install gdal-bin libmemcached-dev - name: Install Django and its Python test dependencies run: | cd django_repo/tests/ diff --git a/django_mongodb_backend_gis/__init__.py b/django_mongodb_backend_gis/__init__.py new file mode 100644 index 00000000..8d5c602e --- /dev/null +++ b/django_mongodb_backend_gis/__init__.py @@ -0,0 +1,3 @@ +from .functions import register_functions + +register_functions() diff --git a/django_mongodb_backend_gis/adapter.py b/django_mongodb_backend_gis/adapter.py new file mode 100644 index 00000000..22a050f2 --- /dev/null +++ b/django_mongodb_backend_gis/adapter.py @@ -0,0 +1,12 @@ +import collections + + +class Adapter(collections.UserDict): + def __init__(self, obj, geography=False): + """ + Initialize on the spatial object. + """ + self.data = { + "type": obj.__class__.__name__, + "coordinates": obj.coords, + } diff --git a/django_mongodb_backend_gis/base.py b/django_mongodb_backend_gis/base.py new file mode 100644 index 00000000..b9eda04d --- /dev/null +++ b/django_mongodb_backend_gis/base.py @@ -0,0 +1,11 @@ +from django_mongodb_backend.base import DatabaseWrapper as BaseDatabaseWrapper + +from .features import DatabaseFeatures +from .operations import DatabaseOperations +from .schema import DatabaseSchemaEditor + + +class DatabaseWrapper(BaseDatabaseWrapper): + SchemaEditorClass = DatabaseSchemaEditor + features_class = DatabaseFeatures + ops_class = DatabaseOperations diff --git a/django_mongodb_backend_gis/features.py b/django_mongodb_backend_gis/features.py new file mode 100644 index 00000000..ac98d414 --- /dev/null +++ b/django_mongodb_backend_gis/features.py @@ -0,0 +1,71 @@ +from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.utils.functional import cached_property + +from django_mongodb_backend.features import DatabaseFeatures as MongoFeatures + + +class DatabaseFeatures(BaseSpatialFeatures, MongoFeatures): + has_spatialrefsys_table = False + supports_transform = False + + @cached_property + def django_test_expected_failures(self): + expected_failures = super().django_test_expected_failures + expected_failures.update( + { + # SRIDs aren't supported. + "gis_tests.geogapp.tests.GeographyTest.test05_geography_layermapping", + "gis_tests.geoapp.tests.GeoModelTest.test_proxy", + # 'WithinLookup' object has no attribute 'as_mql' + # "gis_tests.relatedapp.tests.RelatedGeoModelTest.test10_combine", + # GEOSException: Calling transform() with no SRID set is not supported + "gis_tests.relatedapp.tests.RelatedGeoModelTest.test06_f_expressions", + # 'Adapter' object has no attribute 'srid' + "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_geometry_value_annotation", + # To triage: + "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_multiple_annotation", + # 'ContainsLookup' object has no attribute 'as_mql'. + "gis_tests.geoapp.test_regress.GeoRegressionTests.test_empty_count", + "gis_tests.geoapp.tests.GeoLookupTest.test_contains_contained_lookups", + # Object of type ObjectId is not JSON serializable + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option", + # LinearRing requires at least 4 points, got 1. + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", + "gis_tests.geoapp.tests.GeoLookupTest.test_null_geometries", + # source and target must be of type SpatialReference + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_id_field_option", + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_serialization_base", + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_srid_option", + # 'DisjointLookup' object has no attribute 'as_mql' + "gis_tests.geoapp.tests.GeoLookupTest.test_disjoint_lookup", + # 'SameAsLookup' object has no attribute 'as_mql' + "gis_tests.geoapp.tests.GeoLookupTest.test_equals_lookups", + # 'WithinLookup' object has no attribute 'as_mql' + "gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation", + "gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery", + # issubclass() arg 1 must be a class + "gis_tests.geoapp.tests.GeoModelTest.test_geometryfield", + # KeyError: 'within' connection.ops.gis_operators[self.lookup_name] + "gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string", + # Point object != Point object + "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_update_from_other_field", + # No lookups are supported (yet?) + "gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions", + } + ) + return expected_failures + + @cached_property + def django_test_skips(self): + skips = super().django_test_skips + skips.update( + { + "inspectdb not supported.": { + "gis_tests.inspectapp.tests.InspectDbTests", + }, + "Raw SQL not supported": { + "gis_tests.geoapp.tests.GeoModelTest.test_raw_sql_query", + }, + }, + ) + return skips diff --git a/django_mongodb_backend_gis/functions.py b/django_mongodb_backend_gis/functions.py new file mode 100644 index 00000000..3807f9c6 --- /dev/null +++ b/django_mongodb_backend_gis/functions.py @@ -0,0 +1,6 @@ +# Placeholder if we can support any functions. +# from django.contrib.gis.db.models.functions import Distance, Length, Perimeter + + +def register_functions(): + pass diff --git a/django_mongodb_backend_gis/operations.py b/django_mongodb_backend_gis/operations.py new file mode 100644 index 00000000..a462e915 --- /dev/null +++ b/django_mongodb_backend_gis/operations.py @@ -0,0 +1,85 @@ +from django.contrib.gis import geos +from django.contrib.gis.db import models +from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations + +from django_mongodb_backend.operations import ( + DatabaseOperations as MongoOperations, +) + +from .adapter import Adapter + + +class DatabaseOperations(BaseSpatialOperations, MongoOperations): + Adapter = Adapter + + disallowed_aggregates = ( + models.Collect, + models.Extent, + models.Extent3D, + models.MakeLine, + models.Union, + ) + + @property + def gis_operators(self): + return {} + + unsupported_functions = { + "Area", + "AsGeoJSON", + "AsGML", + "AsKML", + "AsSVG", + "AsWKB", + "AsWKT", + "Azimuth", + "BoundingCircle", + "Centroid", + "ClosestPoint", + "Difference", + "Distance", + "Envelope", + "ForcePolygonCW", + "FromWKB", + "FromWKT", + "GeoHash", + "GeometryDistance", + "Intersection", + "IsEmpty", + "IsValid", + "Length", + "LineLocatePoint", + "MakeValid", + "MemSize", + "NumGeometries", + "NumPoints", + "Perimeter", + "PointOnSurface", + "Reverse", + "Scale", + "SnapToGrid", + "SymDifference", + "Transform", + "Translate", + "Union", + } + + def geo_db_type(self, f): + return "object" + + def get_geometry_converter(self, expression): + geom_class = expression.output_field.geom_class + + def converter(value, expression, connection): # noqa: ARG001 + if value is None: + return None + if issubclass(geom_class, geos.GeometryCollection): + init_val = [ + geom_class._allowed(value["coordinates"][x][0]) + for x in range(len(value["coordinates"])) + ] + else: + init_val = value["coordinates"] + return geom_class(init_val) + + return converter diff --git a/django_mongodb_backend_gis/schema.py b/django_mongodb_backend_gis/schema.py new file mode 100644 index 00000000..bef97a14 --- /dev/null +++ b/django_mongodb_backend_gis/schema.py @@ -0,0 +1,5 @@ +from django_mongodb_backend.schema import DatabaseSchemaEditor as BaseSchemaEditor + + +class DatabaseSchemaEditor(BaseSchemaEditor): + pass From 595c9087dc0cf0c7e4e27ebb932e2bb85388576d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 26 May 2025 22:40:41 -0400 Subject: [PATCH 3/8] some issues triaged --- django_mongodb_backend_gis/__init__.py | 2 ++ django_mongodb_backend_gis/features.py | 18 +++--------------- django_mongodb_backend_gis/lookups.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 django_mongodb_backend_gis/lookups.py diff --git a/django_mongodb_backend_gis/__init__.py b/django_mongodb_backend_gis/__init__.py index 8d5c602e..8cf3b45c 100644 --- a/django_mongodb_backend_gis/__init__.py +++ b/django_mongodb_backend_gis/__init__.py @@ -1,3 +1,5 @@ from .functions import register_functions +from .lookups import register_lookups register_functions() +register_lookups() diff --git a/django_mongodb_backend_gis/features.py b/django_mongodb_backend_gis/features.py index ac98d414..1205cec4 100644 --- a/django_mongodb_backend_gis/features.py +++ b/django_mongodb_backend_gis/features.py @@ -16,17 +16,10 @@ def django_test_expected_failures(self): # SRIDs aren't supported. "gis_tests.geogapp.tests.GeographyTest.test05_geography_layermapping", "gis_tests.geoapp.tests.GeoModelTest.test_proxy", - # 'WithinLookup' object has no attribute 'as_mql' - # "gis_tests.relatedapp.tests.RelatedGeoModelTest.test10_combine", # GEOSException: Calling transform() with no SRID set is not supported "gis_tests.relatedapp.tests.RelatedGeoModelTest.test06_f_expressions", # 'Adapter' object has no attribute 'srid' "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_geometry_value_annotation", - # To triage: - "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_multiple_annotation", - # 'ContainsLookup' object has no attribute 'as_mql'. - "gis_tests.geoapp.test_regress.GeoRegressionTests.test_empty_count", - "gis_tests.geoapp.tests.GeoLookupTest.test_contains_contained_lookups", # Object of type ObjectId is not JSON serializable "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option", # LinearRing requires at least 4 points, got 1. @@ -36,14 +29,9 @@ def django_test_expected_failures(self): "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_id_field_option", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_serialization_base", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_srid_option", - # 'DisjointLookup' object has no attribute 'as_mql' - "gis_tests.geoapp.tests.GeoLookupTest.test_disjoint_lookup", - # 'SameAsLookup' object has no attribute 'as_mql' - "gis_tests.geoapp.tests.GeoLookupTest.test_equals_lookups", - # 'WithinLookup' object has no attribute 'as_mql' - "gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation", - "gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery", - # issubclass() arg 1 must be a class + # GeometryField is not supported (the type of Geometry isn't + # stored so that it can be initialized by the database converter). + # Error in database converter: issubclass() arg 1 must be a class "gis_tests.geoapp.tests.GeoModelTest.test_geometryfield", # KeyError: 'within' connection.ops.gis_operators[self.lookup_name] "gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string", diff --git a/django_mongodb_backend_gis/lookups.py b/django_mongodb_backend_gis/lookups.py new file mode 100644 index 00000000..29c2e1e9 --- /dev/null +++ b/django_mongodb_backend_gis/lookups.py @@ -0,0 +1,10 @@ +from django.contrib.gis.db.models.lookups import GISLookup +from django.db import NotSupportedError + + +def gis_lookup(self, compiler, connection): # noqa: ARG001 + raise NotSupportedError(f"MongoDB does not support the {self.lookup_name} lookup.") + + +def register_lookups(): + GISLookup.as_mql = gis_lookup From 600544fa674bf21b837a4fee9ed93060188b142a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 24 May 2025 21:37:01 -0400 Subject: [PATCH 4/8] Add support for GeoSpatial indexes --- django_mongodb_backend/introspection.py | 4 +- django_mongodb_backend_gis/schema.py | 64 ++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/django_mongodb_backend/introspection.py b/django_mongodb_backend/introspection.py index 77e06807..714f2246 100644 --- a/django_mongodb_backend/introspection.py +++ b/django_mongodb_backend/introspection.py @@ -1,12 +1,12 @@ from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.models import Index -from pymongo import ASCENDING, DESCENDING +from pymongo import ASCENDING, DESCENDING, GEOSPHERE from django_mongodb_backend.indexes import SearchIndex, VectorSearchIndex class DatabaseIntrospection(BaseDatabaseIntrospection): - ORDER_DIR = {ASCENDING: "ASC", DESCENDING: "DESC"} + ORDER_DIR = {ASCENDING: "ASC", DESCENDING: "DESC", GEOSPHERE: "GEO"} def table_names(self, cursor=None, include_views=False): return sorted([x["name"] for x in self.connection.database.list_collections()]) diff --git a/django_mongodb_backend_gis/schema.py b/django_mongodb_backend_gis/schema.py index bef97a14..b333da72 100644 --- a/django_mongodb_backend_gis/schema.py +++ b/django_mongodb_backend_gis/schema.py @@ -1,5 +1,67 @@ +from django.contrib.gis.db.models import GeometryField +from pymongo import GEOSPHERE +from pymongo.operations import IndexModel + from django_mongodb_backend.schema import DatabaseSchemaEditor as BaseSchemaEditor class DatabaseSchemaEditor(BaseSchemaEditor): - pass + def _field_should_be_indexed(self, model, field): + if getattr(field, "spatial_index", False): + return True + return super()._field_should_be_indexed(model, field) + + def _add_field_index(self, model, field, *, column_prefix=""): + if hasattr(field, "geodetic"): + self._add_spatial_index(model, field) + else: + super()._add_field_index(model, field, column_prefix=column_prefix) + + def _alter_field( + self, + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=False, + ): + super()._alter_field( + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=strict, + ) + + old_field_spatial_index = isinstance(old_field, GeometryField) and old_field.spatial_index + new_field_spatial_index = isinstance(new_field, GeometryField) and new_field.spatial_index + if not old_field_spatial_index and new_field_spatial_index: + self._add_spatial_index(model, new_field) + elif old_field_spatial_index and not new_field_spatial_index: + self._delete_spatial_index(model, new_field) + + def _add_spatial_index(self, model, field): + index_name = self._create_spatial_index_name(model, field) + self.get_collection(model._meta.db_table).create_indexes( + [ + IndexModel( + [ + (field.column, GEOSPHERE), + ], + name=index_name, + ) + ] + ) + + def _delete_spatial_index(self, model, field): + index_name = self._create_spatial_index_name(model, field) + self.get_collection(model._meta.db_table).drop_index(index_name) + + def _create_spatial_index_name(self, model, field): + return f"{model._meta.db_table}_{field.column}_id" From 28699bccfd20081d26bee0472aeff0aca6ba2619 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 26 May 2025 22:33:36 -0400 Subject: [PATCH 5/8] disable spatial indexes --- django_mongodb_backend_gis/schema.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/django_mongodb_backend_gis/schema.py b/django_mongodb_backend_gis/schema.py index b333da72..b9b32fdd 100644 --- a/django_mongodb_backend_gis/schema.py +++ b/django_mongodb_backend_gis/schema.py @@ -6,16 +6,16 @@ class DatabaseSchemaEditor(BaseSchemaEditor): - def _field_should_be_indexed(self, model, field): - if getattr(field, "spatial_index", False): - return True - return super()._field_should_be_indexed(model, field) + # def _field_should_be_indexed(self, model, field): + # if getattr(field, "spatial_index", False): + # return True + # return super()._field_should_be_indexed(model, field) - def _add_field_index(self, model, field, *, column_prefix=""): - if hasattr(field, "geodetic"): - self._add_spatial_index(model, field) - else: - super()._add_field_index(model, field, column_prefix=column_prefix) + # def _add_field_index(self, model, field, *, column_prefix=""): + # if hasattr(field, "geodetic"): + # self._add_spatial_index(model, field) + # else: + # super()._add_field_index(model, field, column_prefix=column_prefix) def _alter_field( self, From c9364252b8f62409bee6db8f174bfa1fc67f7723 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 29 May 2025 16:20:41 -0400 Subject: [PATCH 6/8] fix polygon initialization, use serialized type for geom_class --- django_mongodb_backend_gis/adapter.py | 2 +- django_mongodb_backend_gis/features.py | 16 ++++++++-------- django_mongodb_backend_gis/operations.py | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/django_mongodb_backend_gis/adapter.py b/django_mongodb_backend_gis/adapter.py index 22a050f2..a3e7e258 100644 --- a/django_mongodb_backend_gis/adapter.py +++ b/django_mongodb_backend_gis/adapter.py @@ -8,5 +8,5 @@ def __init__(self, obj, geography=False): """ self.data = { "type": obj.__class__.__name__, - "coordinates": obj.coords, + "coordinates": obj.coords[0] if obj.__class__.__name__ == "Polygon" else obj.coords, } diff --git a/django_mongodb_backend_gis/features.py b/django_mongodb_backend_gis/features.py index 1205cec4..28e4e8c5 100644 --- a/django_mongodb_backend_gis/features.py +++ b/django_mongodb_backend_gis/features.py @@ -13,8 +13,8 @@ def django_test_expected_failures(self): expected_failures = super().django_test_expected_failures expected_failures.update( { - # SRIDs aren't supported. - "gis_tests.geogapp.tests.GeographyTest.test05_geography_layermapping", + # SRIDs aren't populated: AssertionError: 4326 != None + # self.assertEqual(4326, nullcity.point.srid) "gis_tests.geoapp.tests.GeoModelTest.test_proxy", # GEOSException: Calling transform() with no SRID set is not supported "gis_tests.relatedapp.tests.RelatedGeoModelTest.test06_f_expressions", @@ -22,17 +22,17 @@ def django_test_expected_failures(self): "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_geometry_value_annotation", # Object of type ObjectId is not JSON serializable "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option", - # LinearRing requires at least 4 points, got 1. - "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", - "gis_tests.geoapp.tests.GeoLookupTest.test_null_geometries", # source and target must be of type SpatialReference + "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_id_field_option", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_serialization_base", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_srid_option", - # GeometryField is not supported (the type of Geometry isn't - # stored so that it can be initialized by the database converter). - # Error in database converter: issubclass() arg 1 must be a class + # GeometryCollection not supported "gis_tests.geoapp.tests.GeoModelTest.test_geometryfield", + "gis_tests.geogapp.tests.GeographyTest.test05_geography_layermapping", + "gis_tests.layermap.tests.LayerMapTest.test_layermap_unique_multigeometry_fk", + "gis_tests.layermap.tests.LayerMapTest.test_null_geom_with_unique", + "gis_tests.layermap.tests.LayerMapTest.test_test_fid_range_step", # KeyError: 'within' connection.ops.gis_operators[self.lookup_name] "gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string", # Point object != Point object diff --git a/django_mongodb_backend_gis/operations.py b/django_mongodb_backend_gis/operations.py index a462e915..38701625 100644 --- a/django_mongodb_backend_gis/operations.py +++ b/django_mongodb_backend_gis/operations.py @@ -68,18 +68,18 @@ def geo_db_type(self, f): return "object" def get_geometry_converter(self, expression): - geom_class = expression.output_field.geom_class - def converter(value, expression, connection): # noqa: ARG001 if value is None: return None + geom_class = getattr(geos, value["type"]) if issubclass(geom_class, geos.GeometryCollection): - init_val = [ - geom_class._allowed(value["coordinates"][x][0]) - for x in range(len(value["coordinates"])) - ] - else: - init_val = value["coordinates"] - return geom_class(init_val) + from django.db import NotSupportedError + + raise NotSupportedError("GeometryCollection not supported") + # init_val = [ + # geom_class._allowed(value["coordinates"][x][0]) + # for x in range(len(value["coordinates"])) + # ] + return geom_class(value["coordinates"]) return converter From 302017334b53d5dbe08d0b5f0c566f78660f92cc Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 29 May 2025 16:24:52 -0400 Subject: [PATCH 7/8] add back GeometryCollection, initialize with srid=4326 --- django_mongodb_backend_gis/features.py | 12 ++---------- django_mongodb_backend_gis/operations.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/django_mongodb_backend_gis/features.py b/django_mongodb_backend_gis/features.py index 28e4e8c5..993263a8 100644 --- a/django_mongodb_backend_gis/features.py +++ b/django_mongodb_backend_gis/features.py @@ -16,27 +16,19 @@ def django_test_expected_failures(self): # SRIDs aren't populated: AssertionError: 4326 != None # self.assertEqual(4326, nullcity.point.srid) "gis_tests.geoapp.tests.GeoModelTest.test_proxy", - # GEOSException: Calling transform() with no SRID set is not supported + # MongoDB does not support the within lookup "gis_tests.relatedapp.tests.RelatedGeoModelTest.test06_f_expressions", # 'Adapter' object has no attribute 'srid' "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_geometry_value_annotation", # Object of type ObjectId is not JSON serializable "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option", - # source and target must be of type SpatialReference "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", - "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_id_field_option", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_serialization_base", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_srid_option", - # GeometryCollection not supported + # Crash initializing GeometryCollection: 'tuple' object is not callable "gis_tests.geoapp.tests.GeoModelTest.test_geometryfield", - "gis_tests.geogapp.tests.GeographyTest.test05_geography_layermapping", - "gis_tests.layermap.tests.LayerMapTest.test_layermap_unique_multigeometry_fk", - "gis_tests.layermap.tests.LayerMapTest.test_null_geom_with_unique", - "gis_tests.layermap.tests.LayerMapTest.test_test_fid_range_step", # KeyError: 'within' connection.ops.gis_operators[self.lookup_name] "gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string", - # Point object != Point object - "gis_tests.geoapp.test_expressions.GeoExpressionsTests.test_update_from_other_field", # No lookups are supported (yet?) "gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions", } diff --git a/django_mongodb_backend_gis/operations.py b/django_mongodb_backend_gis/operations.py index 38701625..1f601417 100644 --- a/django_mongodb_backend_gis/operations.py +++ b/django_mongodb_backend_gis/operations.py @@ -73,13 +73,14 @@ def converter(value, expression, connection): # noqa: ARG001 return None geom_class = getattr(geos, value["type"]) if issubclass(geom_class, geos.GeometryCollection): - from django.db import NotSupportedError - - raise NotSupportedError("GeometryCollection not supported") - # init_val = [ - # geom_class._allowed(value["coordinates"][x][0]) - # for x in range(len(value["coordinates"])) - # ] - return geom_class(value["coordinates"]) + # TODO: confirm this is correct. + return geom_class( + [ + geom_class._allowed(value["coordinates"][x][0]) + for x in range(len(value["coordinates"])) + ], + srid=4326, + ) + return geom_class(value["coordinates"], srid=4326) return converter From a6b6796817b49276ba744e81cfdd75b225c9a81d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 31 May 2025 20:39:24 -0400 Subject: [PATCH 8/8] fix GeometryCollection initialization --- django_mongodb_backend_gis/adapter.py | 11 ++++++++++- django_mongodb_backend_gis/features.py | 2 -- django_mongodb_backend_gis/operations.py | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/django_mongodb_backend_gis/adapter.py b/django_mongodb_backend_gis/adapter.py index a3e7e258..e3eba4a4 100644 --- a/django_mongodb_backend_gis/adapter.py +++ b/django_mongodb_backend_gis/adapter.py @@ -6,7 +6,16 @@ def __init__(self, obj, geography=False): """ Initialize on the spatial object. """ - self.data = { + if obj.__class__.__name__ == "GeometryCollection": + self.data = { + "type": obj.__class__.__name__, + "geometries": [self.get_data(x) for x in obj], + } + else: + self.data = self.get_data(obj) + + def get_data(self, obj): + return { "type": obj.__class__.__name__, "coordinates": obj.coords[0] if obj.__class__.__name__ == "Polygon" else obj.coords, } diff --git a/django_mongodb_backend_gis/features.py b/django_mongodb_backend_gis/features.py index 993263a8..f19d08da 100644 --- a/django_mongodb_backend_gis/features.py +++ b/django_mongodb_backend_gis/features.py @@ -25,8 +25,6 @@ def django_test_expected_failures(self): "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_serialization_base", "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_srid_option", - # Crash initializing GeometryCollection: 'tuple' object is not callable - "gis_tests.geoapp.tests.GeoModelTest.test_geometryfield", # KeyError: 'within' connection.ops.gis_operators[self.lookup_name] "gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string", # No lookups are supported (yet?) diff --git a/django_mongodb_backend_gis/operations.py b/django_mongodb_backend_gis/operations.py index 1f601417..faeda007 100644 --- a/django_mongodb_backend_gis/operations.py +++ b/django_mongodb_backend_gis/operations.py @@ -72,10 +72,17 @@ def converter(value, expression, connection): # noqa: ARG001 if value is None: return None geom_class = getattr(geos, value["type"]) + if geom_class.__name__ == "GeometryCollection": + return geom_class( + [getattr(geos, v["type"])(v["coordinates"]) for v in value["geometries"]], + srid=4326, + ) if issubclass(geom_class, geos.GeometryCollection): # TODO: confirm this is correct. return geom_class( [ + # TODO: For MultiLineString, geom_class._allowed is a + # tuple so this will crash. geom_class._allowed(value["coordinates"][x][0]) for x in range(len(value["coordinates"])) ],