API for querying positions

This commit is contained in:
Laura Klünder 2023-11-24 01:05:38 +01:00
parent 9c6d7989c0
commit 715d6c2f11
5 changed files with 202 additions and 30 deletions

View file

@ -536,6 +536,8 @@ class DynamicLocation(CustomLocationProxyMixin, SpecificLocation, models.Model):
return { return {
'available': False, 'available': False,
'id': self.pk, 'id': self.pk,
'slug': self.slug,
'icon': self.get_icon(),
'title': self.title, 'title': self.title,
'subtitle': '%s %s, %s' % (_('currently unavailable'), _('(moving)'), self.subtitle) 'subtitle': '%s %s, %s' % (_('currently unavailable'), _('(moving)'), self.subtitle)
} }

View file

@ -3,6 +3,7 @@ from typing import Annotated, Union
from django.core.cache import cache from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from ninja import Query from ninja import Query
from ninja import Router as APIRouter from ninja import Router as APIRouter
@ -15,10 +16,11 @@ from c3nav.api.newauth import auth_responses, validate_responses
from c3nav.api.utils import NonEmptyStr from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.models import Source from c3nav.mapdata.models import Source
from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.models.locations import LocationRedirect from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
from c3nav.mapdata.schemas.model_base import LocationID from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID
from c3nav.mapdata.schemas.models import FullLocationSchema, LocationDisplay, SlimLocationSchema from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema,
LocationDisplay, SlimListableLocationSchema, SlimLocationSchema)
from c3nav.mapdata.schemas.responses import BoundsSchema from c3nav.mapdata.schemas.responses import BoundsSchema
from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request,
searchable_locations_for_request, visible_locations_for_request) searchable_locations_for_request, visible_locations_for_request)
@ -74,14 +76,14 @@ def _location_list(request, detailed: bool, filters: LocationListFilters):
@map_api_router.get('/locations/', @map_api_router.get('/locations/',
response={200: list[SlimLocationSchema], **validate_responses, **auth_responses}, response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses},
summary="Get locations (with most important attributes)") summary="Get locations (with most important attributes)")
def location_list(request, filters: Query[LocationListFilters]): def location_list(request, filters: Query[LocationListFilters]):
return _location_list(request, detailed=False, filters=filters) return _location_list(request, detailed=False, filters=filters)
@map_api_router.get('/locations/full/', @map_api_router.get('/locations/full/',
response={200: list[FullLocationSchema], **validate_responses, **auth_responses}, response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses},
summary="Get locations (with all attributes)") summary="Get locations (with all attributes)")
def location_list_full(request, filters: Query[LocationListFilters]): def location_list_full(request, filters: Query[LocationListFilters]):
return _location_list(request, detailed=True, filters=filters) return _location_list(request, detailed=True, filters=filters)
@ -134,7 +136,7 @@ class ShowRedirects(Schema):
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses}, response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses},
summary="Get location by ID (with most important attributes)", 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") 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], def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
redirects: Query[ShowRedirects]): redirects: Query[ShowRedirects]):
return _location_retrieve( return _location_retrieve(
request, request,
@ -147,7 +149,7 @@ def location_by_id(request, location_id: LocationID, filters: Query[RemoveGeomet
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses}, response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses},
summary="Get location by ID (with all attributes)", 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") 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], def location_by_id_full(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
redirects: Query[ShowRedirects]): redirects: Query[ShowRedirects]):
return _location_retrieve( return _location_retrieve(
request, request,
@ -160,14 +162,13 @@ def location_by_id_full(request, location_id: LocationID, filters: Query[RemoveG
response={200: LocationDisplay, **API404.dict(), **auth_responses}, response={200: LocationDisplay, **API404.dict(), **auth_responses},
summary="Get location display data by ID", 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") 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): def location_by_id_display(request, location_id: AnyLocationID):
return _location_display( return _location_display(
request, request,
get_location_by_id_for_request(location_id, request), get_location_by_id_for_request(location_id, request),
) )
@map_api_router.get('/locations/by-slug/{location_slug}/', @map_api_router.get('/locations/by-slug/{location_slug}/',
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses}, response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses},
summary="Get location by slug (with most important attributes)") summary="Get location by slug (with most important attributes)")
@ -200,3 +201,20 @@ def location_by_slug_display(request, location_slug: NonEmptyStr):
request, request,
get_location_by_slug_for_request(location_slug, request), get_location_by_slug_for_request(location_slug, request),
) )
@map_api_router.get('/get_position/{position_id}/',
response={200: AnyPositionStatusSchema, **API404.dict(), **auth_responses},
summary="a numeric ID for a dynamic location or a string ID for the position secret can be used")
def get_current_position_by_id(request, position_id: AnyPositionID):
location = None
if isinstance(position_id, int) or position_id.isdigit():
location = get_location_by_id_for_request(position_id, request)
if not isinstance(location, DynamicLocation):
raise Http404
if location is None and position_id.startswith('p:'):
try:
location = Position.objects.get(secret=position_id[2:])
except Position.DoesNotExist:
raise Http404
return location.serialize_position()

View file

@ -192,19 +192,30 @@ class SimpleGeometryLocationsSchema(Schema):
) )
LocationID = Union[ CustomLocationID = Annotated[NonEmptyStr, 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"
)]
PositionID = Annotated[NonEmptyStr, 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"
)]
AnyLocationID = Union[
Annotated[PositiveInt, APIField( Annotated[PositiveInt, APIField(
title="location ID", title="location ID",
description="numeric ID of any lcation" description="numeric ID of any lcation"
)], )],
Annotated[NonEmptyStr, APIField( CustomLocationID,
title="custom location ID", PositionID,
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" AnyPositionID = Union[
)], Annotated[PositiveInt, APIField(
Annotated[NonEmptyStr, APIField( title="dynamic location ID",
title="position ID", description="numeric ID of any dynamic lcation"
pattern=r"p:[a-z0-9]+$", )],
description="the ID of a user-defined tracked position is made up of its secret" PositionID,
)],
] ]

View file

@ -3,14 +3,12 @@ from typing import Annotated, ClassVar, Literal, Optional, Union
from ninja import Schema from ninja import Schema
from pydantic import Discriminator from pydantic import Discriminator
from pydantic import Field as APIField from pydantic import Field as APIField
from pydantic import GetJsonSchemaHandler, NonNegativeFloat, PositiveFloat, PositiveInt from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema
from c3nav.api.schema import GeometrySchema from c3nav.api.schema import GeometrySchema, PointSchema
from c3nav.api.utils import NonEmptyStr from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.schemas.model_base import (DjangoModelSchema, LabelSettingsSchema, LocationID, LocationSchema, from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, CustomLocationID, DjangoModelSchema,
LocationSlugSchema, SerializableSchema, LabelSettingsSchema, LocationSchema, PositionID, SerializableSchema,
SimpleGeometryBoundsAndPointSchema, SimpleGeometryBoundsSchema, SimpleGeometryBoundsAndPointSchema, SimpleGeometryBoundsSchema,
SimpleGeometryLocationsSchema, SpecificLocationSchema, TitledSchema, SimpleGeometryLocationsSchema, SpecificLocationSchema, TitledSchema,
WithAccessRestrictionSchema, WithLevelSchema, WithAccessRestrictionSchema, WithLevelSchema,
@ -328,6 +326,88 @@ class AccessRestrictionGroupSchema(WithAccessRestrictionSchema, DjangoModelSchem
pass pass
class CustomLocationSchema(SerializableSchema):
"""
A custom location represents coordinates that have been put in or calculated.
A custom location is a location, so it can be routed to and from.
"""
id: CustomLocationID = APIField(
description="ID representing the coordinates"
)
slug: CustomLocationID = APIField(
description="slug, identical to ID"
)
icon: Optional[NonEmptyStr] = APIField(
default=None,
title="icon name",
description="any material design icon name"
)
title: NonEmptyStr = APIField(
title="title (preferred language)",
description="preferred language based on the Accept-Language header."
)
subtitle: NonEmptyStr = APIField(
title="subtitle (preferred language)",
description="an automatically generated short description for this location. "
"preferred language based on the Accept-Language header."
)
level: PositiveInt = APIField(
description="level ID this custom location is located on"
)
space: Optional[PositiveInt] = APIField(
description="space ID this custom location is located in, if applicable"
)
areas: list[PositiveInt] = APIField(
description="IDs of areas this custom location is located in"
)
grid_square: Optional[NonEmptyStr] = APIField(
default=None,
title="grid square",
description="if a grid is defined and this custom location is within it",
)
near_area: Optional[PositiveInt] = APIField(
description="the ID of an area near this custom location, if there is one"
)
near_poi: Optional[PositiveInt] = APIField(
description="the ID of a POI near this custom location, if there is one"
)
neaby: list[PositiveInt] = APIField(
description="list of IDs of nearby locations"
)
altitude: Optional[float] = APIField(
description="ground altitude (in the map-wide coordinate system), if it can be determined"
)
geometry: Optional[PointSchema] = APIField(
description="point geometry for this custom location",
)
class TrackablePositionSchema(Schema):
"""
A trackable position. It's position can be set or reset.
"""
id: PositionID = APIField(
description="ID representing the position"
)
slug: PositionID = APIField(
description="slug representing the position"
)
icon: Optional[NonEmptyStr] = APIField(
default=None,
title="icon name",
description="any material design icon name"
)
title: NonEmptyStr = APIField(
title="title of the position",
)
subtitle: NonEmptyStr = APIField(
title="subtitle (preferred language)",
description="an automatically generated short description, which might change when the position changes. "
"preferred language based on the Accept-Language header."
)
class FullLevelLocationSchema(LevelSchema): class FullLevelLocationSchema(LevelSchema):
""" """
A level for the location API. A level for the location API.
@ -376,6 +456,22 @@ class FullDynamicLocationLocationSchema(SimpleGeometryLocationsSchema, DynamicLo
locationtype: Literal["dynamiclocation"] locationtype: Literal["dynamiclocation"]
class CustomLocationLocationSchema(SimpleGeometryBoundsAndPointSchema, CustomLocationSchema):
"""
A custom location for the location API.
See CustomLocation schema for details.
"""
locationtype: Literal["customlocation"]
class TrackablePositionLocationSchema(TrackablePositionSchema):
"""
A trackable position for the location API.
See TrackablePosition schema for details.
"""
locationtype: Literal["position"]
class SlimLocationMixin(Schema): class SlimLocationMixin(Schema):
level: ClassVar[None] level: ClassVar[None]
space: ClassVar[None] space: ClassVar[None]
@ -443,7 +539,7 @@ class SlimDynamicLocationLocationSchema(SlimLocationMixin, FullDynamicLocationLo
pass pass
FullLocationSchema = Annotated[ FullListableLocationSchema = Annotated[
Union[ Union[
FullLevelLocationSchema, FullLevelLocationSchema,
FullSpaceLocationSchema, FullSpaceLocationSchema,
@ -455,7 +551,16 @@ FullLocationSchema = Annotated[
Discriminator("locationtype"), Discriminator("locationtype"),
] ]
SlimLocationSchema = Annotated[ FullLocationSchema = Annotated[
Union[
FullListableLocationSchema,
CustomLocationLocationSchema,
TrackablePositionLocationSchema,
],
Discriminator("locationtype"),
]
SlimListableLocationSchema = Annotated[
Union[ Union[
SlimLevelLocationSchema, SlimLevelLocationSchema,
SlimSpaceLocationSchema, SlimSpaceLocationSchema,
@ -467,6 +572,15 @@ SlimLocationSchema = Annotated[
Discriminator("locationtype"), Discriminator("locationtype"),
] ]
SlimLocationSchema = Annotated[
Union[
SlimListableLocationSchema,
CustomLocationLocationSchema,
TrackablePositionLocationSchema,
],
Discriminator("locationtype"),
]
class DisplayLink(Schema): class DisplayLink(Schema):
""" """
@ -479,7 +593,7 @@ class DisplayLink(Schema):
class LocationDisplay(SerializableSchema): class LocationDisplay(SerializableSchema):
id: LocationID = APIField( id: AnyLocationID = APIField(
description="a numeric ID for a map location or a string ID for generated locations", description="a numeric ID for a map location or a string ID for generated locations",
) )
level: Optional[PositiveInt] = APIField( level: Optional[PositiveInt] = APIField(
@ -507,3 +621,29 @@ class LocationDisplay(SerializableSchema):
editor_url: Optional[NonEmptyStr] = APIField( editor_url: Optional[NonEmptyStr] = APIField(
None, description="path to edit this object in the editor, if the user has access to it", None, description="path to edit this object in the editor, if the user has access to it",
) )
class PositionStatusSchema(Schema):
id: AnyPositionID = APIField(
description="the ID of the dynamic position that has been queries",
)
slug: NonEmptyStr = APIField(
description="a description for the dynamic position that has been queried"
)
class PositionUnavailableStatusSchema(PositionStatusSchema, TrackablePositionSchema):
available: Literal[False]
class PositionAvailableStatusSchema(PositionStatusSchema, TrackablePositionSchema, CustomLocationSchema):
available: Literal[True]
AnyPositionStatusSchema = Annotated[
Union[
Annotated[PositionUnavailableStatusSchema, APIField(title="position is unavailable")],
Annotated[PositionAvailableStatusSchema, APIField(title="position is available")],
],
Discriminator("available"),
]

View file

@ -312,7 +312,8 @@ class CustomLocation:
('near_area', self.near_area.pk if self.near_area else None), ('near_area', self.near_area.pk if self.near_area else None),
('near_poi', self.near_poi.pk if self.near_poi else None), ('near_poi', self.near_poi.pk if self.near_poi else None),
('nearby', tuple(location.pk for location in self.nearby)), ('nearby', tuple(location.pk for location in self.nearby)),
('altitude', None if self.altitude is None else round(self.altitude, 2)) ('altitude', None if self.altitude is None else round(self.altitude, 2)),
('locationtype', 'customlocation'),
)) ))
if hasattr(self, 'score'): if hasattr(self, 'score'):
result['score'] = self.score result['score'] = self.score