From b47e97bb8112c4cfe7ce83c07da48826e5b06dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Tue, 3 Dec 2024 18:42:33 +0100 Subject: [PATCH] new serializer for all locations --- src/c3nav/api/schema.py | 2 + src/c3nav/mapdata/api/map.py | 25 ++++---- src/c3nav/mapdata/api/mapdata.py | 8 ++- src/c3nav/mapdata/models/geometry/space.py | 4 +- src/c3nav/mapdata/models/locations.py | 4 ++ src/c3nav/mapdata/schemas/filters.py | 3 +- src/c3nav/mapdata/schemas/model_base.py | 14 ++++- src/c3nav/mapdata/schemas/models.py | 68 ++++++++++++++-------- src/c3nav/mapdata/utils/locations.py | 4 +- 9 files changed, 87 insertions(+), 45 deletions(-) diff --git a/src/c3nav/api/schema.py b/src/c3nav/api/schema.py index a756982d..e15ec589 100644 --- a/src/c3nav/api/schema.py +++ b/src/c3nav/api/schema.py @@ -27,6 +27,8 @@ def make_serializable(values: Any): for key, val in values.items() } if isinstance(values, (list, tuple, set, frozenset)): + if values and isinstance(next(iter(values)), Model): + return type(values)(val.pk for val in values) return type(values)(make_serializable(val) for val in values) if isinstance(values, Promise): return str(values) diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index 10f52c98..4fbe7cc1 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -77,17 +77,17 @@ class LocationListFilters(BySearchableFilter, RemoveGeometryFilter): pass -def _location_list(request, detailed: bool, filters: LocationListFilters): +def _location_list(request, filters: LocationListFilters): if filters.searchable: locations = searchable_locations_for_request(request) else: locations = visible_locations_for_request(request).values() - result = [obj.serialize(detailed=detailed, search=filters.searchable, - geometry=filters.geometry and can_access_geometry(request, obj), - simple_geometry=True) - for obj in locations] - return result + for location in locations: + if not filters.geometry or not can_access_geometry(request, location): + location._hide_geometry = True + + return locations @map_api_router.get('/locations/', summary="list locations (slim)", @@ -96,7 +96,7 @@ def _location_list(request, detailed: bool, filters: LocationListFilters): response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses}) @api_etag(base_mapdata=True) def location_list(request, filters: Query[LocationListFilters]): - return _location_list(request, detailed=False, filters=filters) + return _location_list(request, filters=filters) @map_api_router.get('/locations/full/', summary="list locations (full)", @@ -105,7 +105,7 @@ def location_list(request, filters: Query[LocationListFilters]): response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses}) @api_etag(base_mapdata=True) def location_list_full(request, filters: Query[LocationListFilters]): - return _location_list(request, detailed=True, filters=filters) + return _location_list(request, filters=filters) def _location_retrieve(request, location, detailed: bool, geometry: bool, show_redirects: bool): @@ -120,11 +120,10 @@ def _location_retrieve(request, location, detailed: bool, geometry: bool, show_r request._target_etag = None request._target_cache_key = None - return location.serialize( - detailed=detailed, - geometry=geometry and can_access_geometry(request, location), - simple_geometry=True - ) + if not geometry or not can_access_geometry(request, location): + location._hide_geometry = True + + return location def _location_display(request, location): diff --git a/src/c3nav/mapdata/api/mapdata.py b/src/c3nav/mapdata/api/mapdata.py index 1725031d..9e01fd61 100644 --- a/src/c3nav/mapdata/api/mapdata.py +++ b/src/c3nav/mapdata/api/mapdata.py @@ -47,9 +47,13 @@ def mapdata_list_endpoint(request, # order_by qs = qs.order_by(*order_by) - # todo: can access geometry… using defer? + result = list(qs) - return qs + for obj in result: + if can_access_geometry(request, obj): + obj._hide_geometry = True + + return result def mapdata_retrieve_endpoint(request, model: Type[Model], **lookups): diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index baa8831b..6fe162e2 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -309,8 +309,10 @@ class LineObstacle(SpaceGeometryMixin, models.Model): class POI(SpaceGeometryMixin, SpecificLocation, models.Model): """ - An point of interest + A point of interest """ + new_serialize = True + geometry = GeometryField('point') class Meta: diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 6f9bc55e..1b8f1b50 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -385,6 +385,8 @@ class LocationGroup(Location, models.Model): self.orig_category_id = self.category_id self.orig_color = self.color + locations = [] + def details_display(self, editor_url=True, **kwargs): result = super().details_display(**kwargs) result['display'].insert(3, (_('Category'), self.category.title)) @@ -522,6 +524,8 @@ class CustomLocationProxyMixin: class DynamicLocation(CustomLocationProxyMixin, SpecificLocation, models.Model): + new_serialize = True + position_secret = models.CharField(_('position secret'), max_length=32, null=True, blank=True) class Meta: diff --git a/src/c3nav/mapdata/schemas/filters.py b/src/c3nav/mapdata/schemas/filters.py index e3a450d2..dece22e6 100644 --- a/src/c3nav/mapdata/schemas/filters.py +++ b/src/c3nav/mapdata/schemas/filters.py @@ -176,8 +176,7 @@ class RemoveGeometryFilter(FilterSchema): # todo: validated true as invalid if not avaiilable for this model def filter_qs(self, request, qs: QuerySet) -> QuerySet: - if ((qs.model in (Building, Space, Door) and not request.user_permissions.can_access_base_mapdata) - or not self.geometry): + if not self.geometry: qs = qs.defer('geometry') return super().filter_qs(request, qs) diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index f7093df2..439998f1 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -1,6 +1,6 @@ import math import re -from typing import Annotated, Optional, Union +from typing import Annotated, Optional, Union, Literal from pydantic import Field as APIField from pydantic import PositiveInt @@ -119,6 +119,12 @@ class LocationSchema(WithAccessRestrictionSchema, TitledSchema, LocationSlugSche example="more search terms", ) + @classmethod + def get_overrides(cls, value) -> dict: + return { + "locationtype": value._meta.model_name, + } + class LabelSettingsSchema(DjangoModelSchema): # todo: add titles back in here """ @@ -145,6 +151,7 @@ class LabelSettingsSchema(DjangoModelSchema): # todo: add titles back in here class SpecificLocationSchema(LocationSchema): grid_square: Union[ Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")], + Annotated[Literal[""], APIField(title="grid square", description="outside of grid")], Annotated[None, APIField(title="null", description="no grid defined or outside of grid")], ] = APIField( default=None, @@ -235,7 +242,10 @@ class WithGeometrySchema(BaseSchema): minx, miny, maxx, maxy = value.geometry.bounds return { **super().get_overrides(value), - "geometry": format_geojson(smart_mapping(value.geometry), rounded=False), + "geometry": ( + format_geojson(smart_mapping(value.geometry), rounded=False) + if not getattr(value, '_hide_geometry', False) else None + ), "point": (value.level_id,) + tuple(round(i, 2) for i in value.point.coords[0]), "bounds": ((int(math.floor(minx)), int(math.floor(miny))), (int(math.ceil(maxx)), int(math.ceil(maxy)))) diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 13b83cb4..470f9a7b 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -1,6 +1,7 @@ -from typing import Annotated, ClassVar, Literal, Optional, Union +from typing import Annotated, ClassVar, Literal, Optional, Union, Any -from pydantic import Discriminator +from django.db.models import Model +from pydantic import Discriminator, Tag from pydantic import Field as APIField from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt @@ -415,6 +416,7 @@ class CustomLocationSchema(BaseSchema): ) grid_square: Union[ Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")], + Annotated[Literal[""], APIField(title="grid square", description="outside of grid")], Annotated[None, APIField(title="null", description="no grid defined or outside of grid")], ] = APIField( default=None, @@ -569,6 +571,8 @@ class SlimLocationMixin(BaseSchema): can_describe: ClassVar[None] groups: ClassVar[None] groups_by_category: ClassVar[None] + geometry: ClassVar[None] + point: ClassVar[None] class SlimLevelLocationSchema(SlimLocationMixin, FullLevelLocationSchema): @@ -628,46 +632,62 @@ class SlimDynamicLocationLocationSchema(SlimLocationMixin, FullDynamicLocationLo pass +def get_locationtype(v: Any): + if isinstance(v, Model): + return v._meta.model_name + return v["locationtype"] + + FullListableLocationSchema = Annotated[ Union[ - FullLevelLocationSchema, - FullSpaceLocationSchema, - FullAreaLocationSchema, - FullPOILocationSchema, - FullLocationGroupLocationSchema, - FullDynamicLocationLocationSchema, + Annotated[FullLevelLocationSchema, Tag("level")], + Annotated[FullSpaceLocationSchema, Tag("space")], + Annotated[FullAreaLocationSchema, Tag("area")], + Annotated[FullPOILocationSchema, Tag("poi")], + Annotated[FullLocationGroupLocationSchema, Tag("locationgroup")], + Annotated[FullDynamicLocationLocationSchema, Tag("dynamiclocation")], ], - Discriminator("locationtype"), + Discriminator(get_locationtype), ] FullLocationSchema = Annotated[ Union[ - FullListableLocationSchema, - CustomLocationLocationSchema, - TrackablePositionLocationSchema, + Annotated[FullLevelLocationSchema, Tag("level")], + Annotated[FullSpaceLocationSchema, Tag("space")], + Annotated[FullAreaLocationSchema, Tag("area")], + Annotated[FullPOILocationSchema, Tag("poi")], + Annotated[FullLocationGroupLocationSchema, Tag("locationgroup")], + Annotated[FullDynamicLocationLocationSchema, Tag("dynamiclocation")], + Annotated[CustomLocationLocationSchema, Tag("customlocation")], + Annotated[TrackablePositionLocationSchema, Tag("position")], ], - Discriminator("locationtype"), + Discriminator(get_locationtype), ] SlimListableLocationSchema = Annotated[ Union[ - SlimLevelLocationSchema, - SlimSpaceLocationSchema, - SlimAreaLocationSchema, - SlimPOILocationSchema, - SlimLocationGroupLocationSchema, - SlimDynamicLocationLocationSchema, + Annotated[SlimLevelLocationSchema, Tag("level")], + Annotated[SlimSpaceLocationSchema, Tag("space")], + Annotated[SlimAreaLocationSchema, Tag("area")], + Annotated[SlimPOILocationSchema, Tag("poi")], + Annotated[SlimLocationGroupLocationSchema, Tag("locationgroup")], + Annotated[SlimDynamicLocationLocationSchema, Tag("dynamiclocation")], ], - Discriminator("locationtype"), + Discriminator(get_locationtype), ] SlimLocationSchema = Annotated[ Union[ - SlimListableLocationSchema, - CustomLocationLocationSchema, - TrackablePositionLocationSchema, + Annotated[SlimLevelLocationSchema, Tag("level")], + Annotated[SlimSpaceLocationSchema, Tag("space")], + Annotated[SlimAreaLocationSchema, Tag("area")], + Annotated[SlimPOILocationSchema, Tag("poi")], + Annotated[SlimLocationGroupLocationSchema, Tag("locationgroup")], + Annotated[SlimDynamicLocationLocationSchema, Tag("dynamiclocation")], + Annotated[CustomLocationLocationSchema, Tag("customlocation")], + Annotated[TrackablePositionLocationSchema, Tag("position")], ], - Discriminator("locationtype"), + Discriminator(get_locationtype), ] listable_location_definitions = schema_definitions( diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 451f313a..f3313779 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -5,7 +5,7 @@ from collections import OrderedDict from dataclasses import dataclass, field from functools import reduce from itertools import chain -from typing import Any, List, Mapping, Optional, Union +from typing import Any, List, Mapping, Optional, Union, ClassVar from django.apps import apps from django.conf import settings @@ -275,6 +275,8 @@ def get_custom_location_for_request(slug: str, request): @dataclass class CustomLocation: + locationtype: ClassVar = "customlocation" + can_search = True can_describe = True access_restriction_id = None