diff --git a/src/c3nav/mapdata/models/geometry/base.py b/src/c3nav/mapdata/models/geometry/base.py index 1a9ae5c4..c71dd9a6 100644 --- a/src/c3nav/mapdata/models/geometry/base.py +++ b/src/c3nav/mapdata/models/geometry/base.py @@ -82,7 +82,7 @@ class GeometryMixin(SerializableMixin): def _serialize(self, geometry=True, simple_geometry=False, **kwargs): result = super()._serialize(simple_geometry=simple_geometry, **kwargs) - if geometry: + if geometry and "geometry" not in self.get_deferred_fields(): result['geometry'] = format_geojson(smart_mapping(self.geometry), rounded=False) if simple_geometry: result['point'] = (self.level_id, ) + tuple(round(i, 2) for i in self.point.coords[0]) diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 987deb88..a21e81af 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -113,7 +113,8 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): result = super().serialize(detailed=detailed, **kwargs) if not detailed: fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square', - 'locations', 'on_top_of', 'label_settings', 'label_override', 'add_search', 'dynamic') + 'locations', 'on_top_of', 'label_settings', 'label_override', 'add_search', 'dynamic', + 'locationtype', 'geometry') result = {name: result[name] for name in fields if name in result} return result diff --git a/src/c3nav/mapdata/newapi/map.py b/src/c3nav/mapdata/newapi/map.py index a9d32ad7..451dd92d 100644 --- a/src/c3nav/mapdata/newapi/map.py +++ b/src/c3nav/mapdata/newapi/map.py @@ -7,7 +7,8 @@ from pydantic import Field as APIField from c3nav.api.newauth import auth_responses from c3nav.mapdata.models import Source from c3nav.mapdata.models.access import AccessPermission -from c3nav.mapdata.schemas.models import AnyLocationSchema +from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter +from c3nav.mapdata.schemas.models import FullLocationSchema, SlimLocationSchema from c3nav.mapdata.schemas.responses import BoundsSchema from c3nav.mapdata.utils.locations import searchable_locations_for_request, visible_locations_for_request @@ -34,29 +35,39 @@ def can_access_geometry(request): return True # todo: implementFd -@map_api_router.get('/locations/', response={200: list[AnyLocationSchema], **auth_responses}, - summary="Get map boundaries") -def location_list(request, params: Query[LocationEndpointParameters]): - # todo: cache, visibility, etc… - searchable = params.searchable - detailed = True # todo: configurable? - geometry = True # todo: configurable +class LocationListFilters(BySearchableFilter, RemoveGeometryFilter): + pass + +def _location_list(request, detailed: bool, filters: LocationListFilters): + # todo: cache, visibility, etc… cache_key = 'mapdata:api:location:list:%d:%s:%d' % ( - searchable + detailed*2 + geometry*4, + filters.searchable + detailed*2 + filters.geometry*4, AccessPermission.cache_key_for_request(request), request.user_permissions.can_access_base_mapdata ) result = cache.get(cache_key, None) if result is None: - if searchable: + if filters.searchable: locations = searchable_locations_for_request(request) else: locations = visible_locations_for_request(request).values() - result = tuple(obj.serialize(detailed=detailed, search=searchable, - geometry=geometry and can_access_geometry(request), + result = tuple(obj.serialize(detailed=detailed, search=filters.searchable, + geometry=filters.geometry and can_access_geometry(request), simple_geometry=True) for obj in locations) cache.set(cache_key, result, 300) return result + + +@map_api_router.get('/locations/', response={200: list[SlimLocationSchema], **auth_responses}, + summary="Get locations (with most important attributes)") +def location_list(request, filters: Query[LocationListFilters]): + return _location_list(request, detailed=False, filters=filters) + + +@map_api_router.get('/locations/full/', response={200: list[FullLocationSchema], **auth_responses}, + summary="Get locations (with all attributes)") +def location_list_full(request, filters: Query[LocationListFilters]): + return _location_list(request, detailed=True, filters=filters) diff --git a/src/c3nav/mapdata/schemas/filters.py b/src/c3nav/mapdata/schemas/filters.py index 0edf7d88..c4f08164 100644 --- a/src/c3nav/mapdata/schemas/filters.py +++ b/src/c3nav/mapdata/schemas/filters.py @@ -134,3 +134,29 @@ class ByOnTopOfFilter(FilterSchema): if self.on_top_of is not None: qs = qs.filter(on_top_of_id=None if self.on_top_of == "null" else self.on_top_of) return super().filter_qs(qs) + + +class BySearchableFilter(FilterSchema): + searchable: bool = APIField( + False, + title='searchable locations only', + description='only show locations that should show up in search' + ) + + def filter_qs(self, qs: QuerySet) -> QuerySet: + if self.searchable is not None: + qs = qs.filter(can_search=True) + return super().filter_qs(qs) + + +class RemoveGeometryFilter(FilterSchema): + geometry: bool = APIField( + False, # todo: should be false + title='include geometry', + description='by default, geometry will be ommited. set to true to include it (if available)' + ) + + def filter_qs(self, qs: QuerySet) -> QuerySet: + if not self.geometry: + qs = qs.defer('geometry') + return super().filter_qs(qs) diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index c2d94be0..658d3046 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, ClassVar, Optional from ninja import Schema from pydantic import Field as APIField @@ -127,20 +127,26 @@ class SpecificLocationSchema(LocationSchema): class WithPolygonGeometrySchema(Schema): - geometry: PolygonSchema = APIField( + geometry: Optional[PolygonSchema] = APIField( + None, title="geometry", + description="can be null if not available or excluded from endpoint", ) class WithLineStringGeometrySchema(Schema): - geometry: LineStringSchema = APIField( + geometry: Optional[LineStringSchema] = APIField( + None, title="geometry", + description="can be null if not available or excluded from endpoint", ) class WithPointGeometrySchema(Schema): - geometry: PointSchema = APIField( + geometry: Optional[PointSchema] = APIField( + None, title="geometry", + description="can be null if not available or excluded from endpoint", ) @@ -177,3 +183,4 @@ class SimpleGeometryLocationsSchema(Schema): description="IDs of all locations that belong to this grouo", example=(1, 2, 3), ) + diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index e6b3999b..136a3b1a 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -1,5 +1,6 @@ -from typing import Annotated, Literal, Optional, Union +from typing import Annotated, ClassVar, Literal, Optional, Union +from ninja import Schema from pydantic import Discriminator from pydantic import Field as APIField from pydantic import GetJsonSchemaHandler, NonNegativeFloat, PositiveFloat, PositiveInt @@ -315,7 +316,7 @@ class AccessRestrictionGroupSchema(WithAccessRestrictionSchema, DjangoModelSchem pass -class TypedLevelSchema(LevelSchema): +class FullLevelLocationSchema(LevelSchema): """ A level for the location API. See Level schema for details. @@ -323,7 +324,7 @@ class TypedLevelSchema(LevelSchema): locationtype: Literal["level"] -class TypedSpaceSchema(SimpleGeometryBoundsAndPointSchema, SpaceSchema): +class FullSpaceLocationSchema(SimpleGeometryBoundsAndPointSchema, SpaceSchema): """ A space with some additional information for the location API. See Space schema for details. @@ -331,7 +332,7 @@ class TypedSpaceSchema(SimpleGeometryBoundsAndPointSchema, SpaceSchema): locationtype: Literal["space"] -class TypedAreaSchema(SimpleGeometryBoundsAndPointSchema, AreaSchema): +class FullAreaLocationSchema(SimpleGeometryBoundsAndPointSchema, AreaSchema): """ An area with some additional information for the location API. See Area schema for details. @@ -339,7 +340,7 @@ class TypedAreaSchema(SimpleGeometryBoundsAndPointSchema, AreaSchema): locationtype: Literal["area"] -class TypedPOISchema(SimpleGeometryBoundsSchema, POISchema): +class FullPOILocationSchema(SimpleGeometryBoundsSchema, POISchema): """ A point of interest with some additional information for the location API. See POI schema for details. @@ -347,21 +348,92 @@ class TypedPOISchema(SimpleGeometryBoundsSchema, POISchema): locationtype: Literal["poi"] -class TypedLocationGroupSchema(SimpleGeometryLocationsSchema, LocationGroupSchema): +class FullLocationGroupLocationSchema(SimpleGeometryLocationsSchema, LocationGroupSchema): """ - A locagroun group with some additional information for the location API. + A location group with some additional information for the location API. See LocationGroup schema for details. """ locationtype: Literal["locationgroup"] -AnyLocationSchema = Annotated[ +class SlimLocationMixin(Schema): + level: ClassVar[None] + space: ClassVar[None] + titles: ClassVar[None] + access_restriction: ClassVar[None] + can_search: ClassVar[None] + can_describe: ClassVar[None] + groups: ClassVar[None] + + +class SlimLevelLocationSchema(SlimLocationMixin, FullLevelLocationSchema): + """ + A level for the location API with some rarely needed fields removed. + See Level schema for details. + """ + short_label: ClassVar[None] + on_top_of: ClassVar[None] + base_altitude: ClassVar[None] + default_height: ClassVar[None] + door_height: ClassVar[None] + + +class SlimSpaceLocationSchema(SlimLocationMixin, FullSpaceLocationSchema): + """ + A space with some rarely needed fields removed and some additional information for the location API. + See Space schema for details. + """ + outside: ClassVar[None] + height: ClassVar[None] + + +class SlimAreaLocationSchema(SlimLocationMixin, FullAreaLocationSchema): + """ + An area with some rarely needed fields removed and some additional information for the location API. + See Area schema for details. + """ + slow_down_factor: ClassVar[None] + + +class SlimPOILocationSchema(SlimLocationMixin, FullPOILocationSchema): + """ + A point of interest with some rarely needed fields removed and some additional information for the location API. + See POI schema for details. + """ + pass + + +class SlimLocationGroupLocationSchema(SlimLocationMixin, FullLocationGroupLocationSchema): + """ + A locagroun group with some rarely needed fields removed and some additional information for the location API. + See LocationGroup schema for details. + """ + category: ClassVar[None] + priority: ClassVar[None] + hierarchy: ClassVar[None] + color: ClassVar[None] + can_report_missing: ClassVar[None] + + + +FullLocationSchema = Annotated[ Union[ - TypedLevelSchema, - TypedSpaceSchema, - TypedAreaSchema, - TypedPOISchema, - TypedLocationGroupSchema, + FullLevelLocationSchema, + FullSpaceLocationSchema, + FullAreaLocationSchema, + FullPOILocationSchema, + FullLocationGroupLocationSchema, + ], + Discriminator("locationtype"), +] + +SlimLocationSchema = Annotated[ + Union[ + SlimLevelLocationSchema, + SlimSpaceLocationSchema, + SlimAreaLocationSchema, + SlimPOILocationSchema, + SlimLocationGroupLocationSchema, ], Discriminator("locationtype"), ]