Skip to content
2 changes: 2 additions & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions strawberry_django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
DateFilterLookup,
DatetimeFilterLookup,
FilterLookup,
GeometryFilterLookup,
RangeLookup,
TimeFilterLookup,
)
Expand Down Expand Up @@ -37,6 +38,8 @@
"DjangoImageType",
"DjangoModelType",
"FilterLookup",
"GeometryFilterLookup",
"GeometryFilterLookup",
"ListInput",
"ManyToManyInput",
"ManyToOneInput",
Expand Down
62 changes: 62 additions & 0 deletions strawberry_django/fields/filter_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@
import decimal
import uuid
from typing import (
TYPE_CHECKING,
Annotated,
Generic,
Optional,
TypeVar,
)

import strawberry
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
from strawberry import UNSET

from strawberry_django.filters import resolve_value

from .filter_order import filter_field

if TYPE_CHECKING:
from .types import Geometry

T = TypeVar("T")

_SKIP_MSG = "Filter will be skipped on `null` value"
Expand Down Expand Up @@ -123,3 +129,59 @@
str: FilterLookup,
uuid.UUID: FilterLookup,
}


GeometryFilterLookup = None

Check failure on line 134 in strawberry_django/fields/filter_types.py

View workflow job for this annotation

GitHub Actions / Typing

Type "None" is not assignable to declared type "type[GeometryFilterLookup[T@GeometryFilterLookup]]"   Type "None" is not assignable to type "type[GeometryFilterLookup[T@GeometryFilterLookup]]" (reportAssignmentType)

try:
pass
Copy link
Member

Choose a reason for hiding this comment

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

todo: hrm, I think this is missing something =P

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that it was removed by auto fixes from pre-commit.com hooks. Originally there was a code to test whether geodjango is available. from django.contrib.gis.geos import GEOSGeometry And I think that it's a better idea to define a configuration variable that indicates geodjango is available or not. But I don't know where it should be located.

Copy link
Contributor

Choose a reason for hiding this comment

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

try:
    from django.contrib.gis.db import models as geos_fields

    GEOS_IMPORTED = True

    class GeosFieldsModel(models.Model):
        point = geos_fields.PointField(null=True, blank=True)
        line_string = geos_fields.LineStringField(null=True, blank=True)
        polygon = geos_fields.PolygonField(null=True, blank=True)
        multi_point = geos_fields.MultiPointField(null=True, blank=True)
        multi_line_string = geos_fields.MultiLineStringField(null=True, blank=True)
        multi_polygon = geos_fields.MultiPolygonField(null=True, blank=True)
        geometry = geos_fields.GeometryField(null=True, blank=True)

except ImproperlyConfigured:
    GEOS_IMPORTED = False

I found this in tests/models.py file. But I think it should be located in somewhere else.

except ImproperlyConfigured:
# If gdal is not available, skip.
pass
else:

@strawberry.input
class GeometryFilterLookup(Generic[T]):
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider dynamically generating the geometry fields to reduce code duplication.

Consider reducing the repetitive field definitions by generating the geometry fields dynamically. For example, you can define a list of method names and then use a helper to build your fields. This will reduce the nesting and repetitive code without changing functionality. For instance:

try:
    from django.contrib.gis.geos import GEOSGeometry
except ImproperlyConfigured:
    # If gdal is not available, skip.
    pass
else:
    GEO_FIELD_TYPE = Optional[Annotated["Geometry", strawberry.lazy(".types")]]
    geometry_methods = [
        "bbcontains", "bboverlaps", "contained", "contains", "contains_properly",
        "coveredby", "covers", "crosses", "disjoint", "equals", "exacts",
        "intersects", "overlaps", "touches", "within", "left", "right",
        "overlaps_left", "overlaps_right", "overlaps_above", "overlaps_below",
        "strictly_above", "strictly_below",
    ]

    def _build_geometry_fields():
        fields = {name: UNSET for name in geometry_methods}
        fields.update({
            "isempty": filter_field(description=f"Test whether it's empty. {_SKIP_MSG}"),
            "isvalid": filter_field(description=f"Test whether it's valid. {_SKIP_MSG}"),
        })
        return fields

    GeometryFields = _build_geometry_fields()
    GeometryFilterLookup = strawberry.input(
        type("GeometryFilterLookup", (Generic[T],), GeometryFields)
    )

This refactoring maintains all functionality while reducing the duplication and nesting in your class definition.

bbcontains: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
bboverlaps: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
contained: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
contains: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
contains_properly: Optional[
Annotated["Geometry", strawberry.lazy(".types")]
] = UNSET
coveredby: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
covers: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
crosses: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
disjoint: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
equals: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
exacts: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
intersects: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
isempty: Optional[bool] = filter_field(
description=f"Test whether it's empty. {_SKIP_MSG}"
)
isvalid: Optional[bool] = filter_field(
description=f"Test whether it's valid. {_SKIP_MSG}"
)
overlaps: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
touches: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
within: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
left: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
right: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = UNSET
overlaps_left: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
overlaps_right: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
overlaps_above: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
overlaps_below: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
strictly_above: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
strictly_below: Optional[Annotated["Geometry", strawberry.lazy(".types")]] = (
UNSET
)
18 changes: 16 additions & 2 deletions strawberry_django/fields/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from strawberry.file_uploads.scalars import Upload
from strawberry.scalars import JSON
from strawberry.types.enum import EnumValueDefinition
from strawberry.types.scalar import ScalarWrapper
from strawberry.utils.str_converters import capitalize_first, to_camel_case

from strawberry_django import filters
Expand Down Expand Up @@ -348,7 +349,7 @@
Geometry = strawberry.scalar(
NewType("Geometry", geos.GEOSGeometry),
serialize=lambda v: v.tuple if isinstance(v, geos.GEOSGeometry) else v, # type: ignore
parse_value=lambda v: geos.GeometryCollection,
parse_value=lambda v: geos.GEOSGeometry(v),
description=(
"An arbitrary geographical object. One of Point, "
"LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon."
Expand Down Expand Up @@ -556,10 +557,23 @@
and (field_type is not bool or not using_old_filters)
):
if using_old_filters:
field_type = filters.FilterLookup[field_type]
field_type = filters.FilterLookup[field_type] # type: ignore

Check warning on line 560 in strawberry_django/fields/types.py

View workflow job for this annotation

GitHub Actions / Typing

Unnecessary "# type: ignore" comment (reportUnnecessaryTypeIgnoreComment)
elif type(
field_type
) is ScalarWrapper and field_type._scalar_definition.name in (

Check failure on line 563 in strawberry_django/fields/types.py

View workflow job for this annotation

GitHub Actions / Typing

Cannot access attribute "_scalar_definition" for class "type[bool]"   Attribute "_scalar_definition" is unknown (reportAttributeAccessIssue)
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: might need a # type: ignore here

Copy link
Contributor

Choose a reason for hiding this comment

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

OK.

"Point",
"LineString",
"LinearRing",
"Polygon",
"MultiPoint",
"MultilineString",
"MultiPolygon",
"Geometry",
):
field_type = filter_types.GeometryFilterLookup[field_type]
else:
field_type = filter_types.type_filter_map.get( # type: ignore
field_type, filter_types.FilterLookup

Check failure on line 576 in strawberry_django/fields/types.py

View workflow job for this annotation

GitHub Actions / Typing

Argument of type "Any | Unknown | type[bool] | ScalarWrapper" cannot be assigned to parameter "key" of type "type[ID] | type[bool] | type[date] | type[datetime] | type[time] | type[Decimal] | type[float] | type[int] | type[str] | type[UUID]" in function "get"   Type "Any | Unknown | type[bool] | ScalarWrapper" is not assignable to type "type[ID] | type[bool] | type[date] | type[datetime] | type[time] | type[Decimal] | type[float] | type[int] | type[str] | type[UUID]"     Type "ScalarWrapper" is not assignable to type "type[ID] | type[bool] | type[date] | type[datetime] | type[time] | type[Decimal] | type[float] | type[int] | type[str] | type[UUID]"       Type "ScalarWrapper" is not assignable to type "type[ID]"       Type "ScalarWrapper" is not assignable to type "type[bool]"       Type "ScalarWrapper" is not assignable to type "type[date]"       Type "ScalarWrapper" is not assignable to type "type[datetime]"       Type "ScalarWrapper" is not assignable to type "type[time]"       Type "ScalarWrapper" is not assignable to type "type[Decimal]" ... (reportArgumentType)
)[field_type]

return field_type
Expand Down
31 changes: 30 additions & 1 deletion tests/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
import strawberry
from asgiref.sync import sync_to_async
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from graphql import GraphQLError
Expand All @@ -15,7 +16,7 @@
import strawberry_django
from strawberry_django.settings import StrawberryDjangoSettings

from . import models, utils
from . import models, types, utils


@pytest.fixture
Expand Down Expand Up @@ -79,6 +80,8 @@ class Query:
fruit: Fruit = strawberry_django.field()
berries: list[BerryFruit] = strawberry_django.field()
bananas: list[BananaFruit] = strawberry_django.field()
if settings.GEOS_IMPORTED:
geometries: list[types.GeoField] = strawberry_django.field()


@pytest.fixture
Expand Down Expand Up @@ -314,3 +317,29 @@ def fruit(self) -> Fruit:
}
""")
assert result.data == {"fruit": {"colorId": mock.ANY, "name": "Banana"}}


@pytest.mark.skipif(not settings.GEOS_IMPORTED, reason="GeoDjango is not available.")
async def test_geos(query):
from django.contrib.gis.geos import GEOSGeometry

result = await query(
"""
query GeosQuery($filter: GeoFieldFilter) {
geometries(filters: $filter) {
geometry
}
}
""",
variable_values={
"filter": {
"geometry": {
"contains": GEOSGeometry(
"POLYGON(( 10 10, 10 20, 20 20, 20 15, 10 10))"
)
}
}
},
)

assert not result.errors
7 changes: 6 additions & 1 deletion tests/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ class TomatoWithRequiredPictureType:

if settings.GEOS_IMPORTED:

@strawberry_django.type(models.GeosFieldsModel)
@strawberry_django.filters.filter(models.GeosFieldsModel, lookups=True)
class GeoFieldFilter:
geometry: auto

@strawberry_django.type(models.GeosFieldsModel, filters=GeoFieldFilter)
class GeoField:
id: auto
point: auto
Expand All @@ -47,6 +51,7 @@ class GeoField:
multi_point: auto
multi_line_string: auto
multi_polygon: auto
geometry: auto

@strawberry_django.input(models.GeosFieldsModel)
class GeoFieldInput(GeoField):
Expand Down
Loading