API for querying positions
This commit is contained in:
parent
9c6d7989c0
commit
715d6c2f11
5 changed files with 202 additions and 30 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
)],
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"),
|
||||||
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue