def get_queryset(self, feature_type: FeatureType) -> QuerySet: """Generate the queryset for the specific feature type. This method can be overwritten in subclasses to define the returned data. However, consider overwriting :meth:`compile_query` instead of simple data. """ queryset = feature_type.get_queryset() # Apply filters compiler = self.compile_query(feature_type, using=queryset.db) if self.value_reference is not None: if feature_type.resolve_element( self.value_reference.xpath) is None: raise InvalidParameterValue( "valueReference", f"Field '{self.value_reference.xpath}' does not exist.", ) # For GetPropertyValue, adjust the query so only that value is requested. # This makes sure XPath attribute selectors are already handled by the # database query, instead of being a presentation-layer handling. field = compiler.add_value_reference(self.value_reference) queryset = compiler.filter_queryset(queryset, feature_type=feature_type) return queryset.values("pk", member=field) else: return compiler.filter_queryset(queryset, feature_type=feature_type)
class PlacesWFSView(WFSView): """An simple view that uses the WFSView against our test model.""" xml_namespace = "http://example.org/gisserver" service_description = ServiceDescription( # While not tested directly, this is still validated against the XSD title="Places", abstract="Unittesting", keywords=["django-gisserver"], provider_name="Django", provider_site="https://www.example.com/", contact_person="django-gisserver", ) feature_types = [ FeatureType( Restaurant.objects.all(), fields="__all__", keywords=["unittest"], other_crs=[RD_NEW], metadata_url="/feature/restaurants/", ), FeatureType( Restaurant.objects.all(), name="mini-restaurant", keywords=["unittest", "limited-fields"], other_crs=[RD_NEW], metadata_url="/feature/restaurants-limit/", ), DeniedFeatureType(Restaurant.objects.none(), name="denied-feature"), ]
def render_feature(self, feature_type: FeatureType, instance: models.Model) -> bytes: """Render the output of a single feature""" # Get all instance attributes: properties = self.get_properties(feature_type.xsd_type, instance) if feature_type.show_name_field: name = feature_type.get_display_value(instance) geometry_name = b'"geometry_name":%b,' % orjson.dumps(name) else: geometry_name = b"" return (b" {" b'"type":"Feature",' b'"id":%b,' b"%b" b'"geometry":%b,' b'"properties":%b' b"}") % ( orjson.dumps(f"{feature_type.name}.{instance.pk}"), geometry_name, self.render_geometry(feature_type, instance), orjson.dumps(properties, default=_json_default), )
def render_xml_field(self, feature_type: FeatureType, xsd_element: XsdElement, value, extra_xmlns="") -> str: """Write the value of a single field.""" if xsd_element.is_many: if value is None: # No tag for optional element (see PropertyIsNull), otherwise xsi:nil node. if xsd_element.min_occurs == 0: return "" else: return f' <{xsd_element.xml_name} xsi:nil="true"{extra_xmlns} />\n' else: # Render the tag multiple times if xsd_element.type.is_complex_type: # If the retrieved QuerySet was not filtered yet, do so now. This can't # be done in get_value() because the FeatureType is not known there. value = feature_type.filter_related_queryset(value) return "".join( self._render_xml_field(feature_type, xsd_element, value=item, extra_xmlns=extra_xmlns) for item in value) else: return self._render_xml_field(feature_type, xsd_element, value, extra_xmlns=extra_xmlns)
def get_prefetch_queryset( cls, feature_type: FeatureType, feature_relation: FeatureRelation, output_crs: CRS, ) -> models.QuerySet | None: """Generate a custom queryset that's used to prefetch a relation.""" # Multiple elements could be referencing the same model, just take first that is filled in. if feature_relation.related_model is None: return None return feature_type.get_related_queryset(feature_relation)
def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params): """Update the queryset to let the database render the GML output.""" value_reference = params["valueReference"] match = feature_type.resolve_element(value_reference.xpath) if match.child.is_geometry: # Add 'gml_member' to point to the pre-rendered GML version. return queryset.values( "pk", gml_member=AsGML(get_db_geometry_target(match, output_crs))) else: return queryset
def decorate_queryset(self, feature_type: FeatureType, queryset, output_crs, **params): """Update the queryset to let the database render the GML output. This is far more efficient then GeoDjango's logic, which performs a C-API call for every single coordinate of a geometry. """ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params) # If desired, the entire FeatureCollection could be rendered # in PostgreSQL as well: https://postgis.net/docs/ST_AsGeoJSON.html match = feature_type.resolve_element(feature_type.geometry_field_name) return queryset.defer(feature_type.geometry_field_name).annotate( _as_db_geojson=AsGeoJSON(get_db_geometry_target(match, output_crs), precision=16))
class FlattenedWFSView(PlacesWFSView): """An advanced view that has a custom type definition for a foreign key.""" feature_types = [ FeatureType( Restaurant.objects.all(), fields=[ "id", "name", field("city-id", model_attribute="city_id"), field("city-name", model_attribute="city.name"), "location", "rating", "is_open", "created", ], ), ]
class ComplexTypesWFSView(PlacesWFSView): """An advanced view that has a custom type definition for a foreign key.""" feature_types = [ FeatureType( Restaurant.objects.all(), fields=[ "id", "name", field("city", fields=["id", "name"]), "location", "rating", "is_open", "created", ], other_crs=[RD_NEW], ), ]
def get_properties( self, feature_type: FeatureType, xsd_type: XsdComplexType, instance: models.Model, ) -> dict: """Collect the data for the 'properties' field. This is based on the original XSD definition, so the rendering is consistent with other output formats. """ props = {} for xsd_element in xsd_type.elements: if not xsd_element.is_geometry: value = xsd_element.get_value(instance) if xsd_element.type.is_complex_type: # Nested object data if value is None: props[xsd_element.name] = None else: value_xsd_type = cast(XsdComplexType, xsd_element.type) if xsd_element.is_many: # If the retrieved QuerySet was not filtered yet, do so now. # This can't be done in get_value() because the FeatureType # is not known there. value = feature_type.filter_related_queryset(value) # "..._to_many relation; reverse FK, M2M or array field. props[xsd_element.name] = [ self.get_properties(feature_type, value_xsd_type, item) for item in value ] else: props[xsd_element.name] = self.get_properties( feature_type, value_xsd_type, value) else: # Scalar value, or list (for ArrayField). props[xsd_element.name] = self._format_geojson_value(value) return props
class ComplexTypesWFSView(PlacesWFSView): """An advanced view that has a custom type definition for a foreign key and M2M relation.""" feature_types = [ FeatureType( Restaurant.objects.all(), fields=[ "id", "name", field("city", fields=["id", "name"]), "location", "rating", "is_open", "created", field("opening_hours", fields=["weekday", "start_time", "end_time"]), "tags", # array field ], other_crs=[RD_NEW], ), ]