diff --git a/src/c3nav/api/schema.py b/src/c3nav/api/schema.py index 0f1ffeab..b8f7cace 100644 --- a/src/c3nav/api/schema.py +++ b/src/c3nav/api/schema.py @@ -1,6 +1,7 @@ -from typing import Literal +from typing import Annotated, Literal, Union from ninja import Schema +from pydantic import Discriminator from pydantic import Field as APIField from c3nav.api.utils import NonEmptyStr @@ -43,3 +44,13 @@ class PointSchema(Schema): coordinates: tuple[float, float] = APIField( example=[1, 2.5] ) + + +GeometrySchema = Annotated[ + Union[ + PolygonSchema, + LineStringSchema, + PointSchema,], + Discriminator("type"), +] + diff --git a/src/c3nav/mapdata/newapi/map.py b/src/c3nav/mapdata/newapi/map.py index 451dd92d..e0289537 100644 --- a/src/c3nav/mapdata/newapi/map.py +++ b/src/c3nav/mapdata/newapi/map.py @@ -1,16 +1,27 @@ +import json +from typing import Annotated, Union + from django.core.cache import cache +from django.core.serializers.json import DjangoJSONEncoder +from django.shortcuts import redirect from ninja import Query from ninja import Router as APIRouter from ninja import Schema from pydantic import Field as APIField +from pydantic import PositiveInt -from c3nav.api.newauth import auth_responses +from c3nav.api.exceptions import API404 +from c3nav.api.newauth import auth_responses, validate_responses from c3nav.mapdata.models import Source from c3nav.mapdata.models.access import AccessPermission +from c3nav.mapdata.models.locations import LocationRedirect from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter -from c3nav.mapdata.schemas.models import FullLocationSchema, SlimLocationSchema +from c3nav.mapdata.schemas.model_base import LocationID +from c3nav.mapdata.schemas.models import FullLocationSchema, LocationDisplay, SlimLocationSchema from c3nav.mapdata.schemas.responses import BoundsSchema -from c3nav.mapdata.utils.locations import searchable_locations_for_request, visible_locations_for_request +from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, searchable_locations_for_request, + visible_locations_for_request) +from c3nav.mapdata.utils.user import can_access_editor map_api_router = APIRouter(tags=["map"]) @@ -61,13 +72,97 @@ def _location_list(request, detailed: bool, filters: LocationListFilters): return result -@map_api_router.get('/locations/', response={200: list[SlimLocationSchema], **auth_responses}, +@map_api_router.get('/locations/', + response={200: list[SlimLocationSchema], **validate_responses, **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}, +@map_api_router.get('/locations/full/', + response={200: list[FullLocationSchema], **validate_responses, **auth_responses}, summary="Get locations (with all attributes)") def location_list_full(request, filters: Query[LocationListFilters]): return _location_list(request, detailed=True, filters=filters) + + +def _location_retrieve(request, location, detailed: bool, geometry: bool, show_redirects: bool): + # todo: cache, visibility, etc… + + if location is None: + raise API404 + + if isinstance(location, LocationRedirect): + if not show_redirects: + return redirect('../' + str(location.target.slug)) # todo: use reverse, make pk and slug both work + + return location.serialize( + detailed=detailed, + geometry=geometry and can_access_geometry(request), + simple_geometry=True + ) + + +def _location_display(request, location): + # todo: cache, visibility, etc… + + if location is None: + raise API404 + + if isinstance(location, LocationRedirect): + return redirect('../' + str(location.target.slug) + '/details/') # todo: use reverse, make pk+slug work + + result = location.details_display( + detailed_geometry=can_access_geometry(request), + editor_url=can_access_editor(request) + ) + from pprint import pprint + pprint(result) + return json.loads(json.dumps(result, cls=DjangoJSONEncoder)) # todo: wtf?? well we need to get rid of lazy strings + + +class ShowRedirects(Schema): + show_redirects: bool = APIField( + False, + name="show redirects", + description="whether to show redirects instead of sending a redirect response", + ) + + +@map_api_router.get('/locations/{location_id}/', + response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses}, + summary="Get location by ID (with most important attributes)", + description="a numeric ID for a map location or a string ID for generated locations can be used") +def location_by_id(request, location_id: LocationID, filters: Query[RemoveGeometryFilter], + redirects: Query[ShowRedirects]): + return _location_retrieve( + request, + get_location_by_id_for_request(location_id, request), + detailed=False, geometry=filters.geometry, show_redirects=redirects.show_redirects, + ) + + +@map_api_router.get('/locations/{location_id}/full/', + response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses}, + summary="Get location by ID (with all attributes)", + description="a numeric ID for a map location or a string ID for generated locations can be used") +def location_by_id_full(request, location_id: LocationID, filters: Query[RemoveGeometryFilter], + redirects: Query[ShowRedirects]): + return _location_retrieve( + request, + get_location_by_id_for_request(location_id, request), + detailed=True, geometry=filters.geometry, show_redirects=redirects.show_redirects, + ) + + +@map_api_router.get('/locations/{location_id}/display/', + response={200: LocationDisplay, **API404.dict(), **auth_responses}, + summary="Get location display data by ID", + description="a numeric ID for a map location or a string ID for generated locations can be used") +def location_by_id_display(request, location_id: LocationID): + return _location_display( + request, + get_location_by_id_for_request(location_id, request), + ) + + diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index 3cb4754a..48673556 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, ClassVar, Optional +from typing import Annotated, Any, ClassVar, Optional, Union from ninja import Schema from pydantic import Field as APIField @@ -189,3 +189,14 @@ class SimpleGeometryLocationsSchema(Schema): example=(1, 2, 3), ) + +LocationID = Union[ + Annotated[int, APIField(title="location ID", + description="numeric ID of any lcation")], + Annotated[str, APIField(title="custom location ID", + pattern=r"c:[a-z0-9-_]+:(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)$", + description="level short_name and x/y coordinates form the ID of a custom location")], + Annotated[str, APIField(title="position ID", + pattern=r"p:[a-z0-9]+$", + description="the ID of a user-defined tracked position is made up of its secret")], +] diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 136a3b1a..9ab2bdda 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -7,12 +7,14 @@ from pydantic import GetJsonSchemaHandler, NonNegativeFloat, PositiveFloat, Posi from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema +from c3nav.api.schema import GeometrySchema from c3nav.api.utils import NonEmptyStr -from c3nav.mapdata.schemas.model_base import (DjangoModelSchema, LabelSettingsSchema, LocationSchema, - LocationSlugSchema, SimpleGeometryBoundsAndPointSchema, - SimpleGeometryBoundsSchema, SimpleGeometryLocationsSchema, - SpecificLocationSchema, TitledSchema, WithAccessRestrictionSchema, - WithLevelSchema, WithLineStringGeometrySchema, WithPointGeometrySchema, +from c3nav.mapdata.schemas.model_base import (DjangoModelSchema, LabelSettingsSchema, LocationID, LocationSchema, + LocationSlugSchema, SerializableSchema, + SimpleGeometryBoundsAndPointSchema, SimpleGeometryBoundsSchema, + SimpleGeometryLocationsSchema, SpecificLocationSchema, TitledSchema, + WithAccessRestrictionSchema, WithLevelSchema, + WithLineStringGeometrySchema, WithPointGeometrySchema, WithPolygonGeometrySchema, WithSpaceSchema) @@ -439,3 +441,42 @@ SlimLocationSchema = Annotated[ ] +class DisplayLink(Schema): + """ + A link for the location display + """ + id: PositiveInt + slug: NonEmptyStr + title: NonEmptyStr + can_search: bool + + +class LocationDisplay(SerializableSchema): + id: LocationID = APIField( + description="a numeric ID for a map location or a string ID for generated locations", + ) + level: Optional[PositiveInt] = APIField( + None, + description="level ID, if applicable" + ) + space: Optional[PositiveInt] = APIField( + None, + description="space ID, if applicable" + ) + display: list[ + tuple[ + Annotated[NonEmptyStr, APIField(name="field title")], + Annotated[Union[ + Annotated[str, APIField(name="a simple string value")], + Annotated[DisplayLink, APIField(namen="a link value")], + Annotated[list[DisplayLink], APIField(name="a list of link values")], + Annotated[Literal[None], APIField(name="no value")] + ], APIField(name="field value")] + ] + ] = APIField(description="a list of human-readable display values") + geometry: Optional[GeometrySchema] = APIField( + None, description="representative geometry, if available" + ) + editor_url: Optional[NonEmptyStr] = APIField( + None, description="path to edit this object in the editor, if the user has access to it", + )