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 {
|
||||
'available': False,
|
||||
'id': self.pk,
|
||||
'slug': self.slug,
|
||||
'icon': self.get_icon(),
|
||||
'title': self.title,
|
||||
'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.serializers.json import DjangoJSONEncoder
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from ninja import Query
|
||||
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.mapdata.models import Source
|
||||
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.model_base import LocationID
|
||||
from c3nav.mapdata.schemas.models import FullLocationSchema, LocationDisplay, SlimLocationSchema
|
||||
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID
|
||||
from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema,
|
||||
LocationDisplay, SlimListableLocationSchema, SlimLocationSchema)
|
||||
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,
|
||||
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/',
|
||||
response={200: list[SlimLocationSchema], **validate_responses, **auth_responses},
|
||||
response={200: list[SlimListableLocationSchema], **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], **validate_responses, **auth_responses},
|
||||
response={200: list[FullListableLocationSchema], **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)
|
||||
|
@ -134,7 +136,7 @@ class ShowRedirects(Schema):
|
|||
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],
|
||||
def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
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},
|
||||
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],
|
||||
def location_by_id_full(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
request,
|
||||
|
@ -160,14 +162,13 @@ def location_by_id_full(request, location_id: LocationID, filters: Query[RemoveG
|
|||
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):
|
||||
def location_by_id_display(request, location_id: AnyLocationID):
|
||||
return _location_display(
|
||||
request,
|
||||
get_location_by_id_for_request(location_id, request),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@map_api_router.get('/locations/by-slug/{location_slug}/',
|
||||
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses},
|
||||
summary="Get location by slug (with most important attributes)")
|
||||
|
@ -200,3 +201,20 @@ def location_by_slug_display(request, location_slug: NonEmptyStr):
|
|||
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(
|
||||
title="location ID",
|
||||
description="numeric ID of any lcation"
|
||||
)],
|
||||
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"
|
||||
)],
|
||||
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"
|
||||
)],
|
||||
CustomLocationID,
|
||||
PositionID,
|
||||
]
|
||||
AnyPositionID = Union[
|
||||
Annotated[PositiveInt, APIField(
|
||||
title="dynamic location ID",
|
||||
description="numeric ID of any dynamic lcation"
|
||||
)],
|
||||
PositionID,
|
||||
]
|
||||
|
|
|
@ -3,14 +3,12 @@ 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
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from pydantic_core import CoreSchema
|
||||
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
|
||||
|
||||
from c3nav.api.schema import GeometrySchema
|
||||
from c3nav.api.schema import GeometrySchema, PointSchema
|
||||
from c3nav.api.utils import NonEmptyStr
|
||||
from c3nav.mapdata.schemas.model_base import (DjangoModelSchema, LabelSettingsSchema, LocationID, LocationSchema,
|
||||
LocationSlugSchema, SerializableSchema,
|
||||
from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, CustomLocationID, DjangoModelSchema,
|
||||
LabelSettingsSchema, LocationSchema, PositionID, SerializableSchema,
|
||||
SimpleGeometryBoundsAndPointSchema, SimpleGeometryBoundsSchema,
|
||||
SimpleGeometryLocationsSchema, SpecificLocationSchema, TitledSchema,
|
||||
WithAccessRestrictionSchema, WithLevelSchema,
|
||||
|
@ -328,6 +326,88 @@ class AccessRestrictionGroupSchema(WithAccessRestrictionSchema, DjangoModelSchem
|
|||
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):
|
||||
"""
|
||||
A level for the location API.
|
||||
|
@ -376,6 +456,22 @@ class FullDynamicLocationLocationSchema(SimpleGeometryLocationsSchema, DynamicLo
|
|||
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):
|
||||
level: ClassVar[None]
|
||||
space: ClassVar[None]
|
||||
|
@ -443,7 +539,7 @@ class SlimDynamicLocationLocationSchema(SlimLocationMixin, FullDynamicLocationLo
|
|||
pass
|
||||
|
||||
|
||||
FullLocationSchema = Annotated[
|
||||
FullListableLocationSchema = Annotated[
|
||||
Union[
|
||||
FullLevelLocationSchema,
|
||||
FullSpaceLocationSchema,
|
||||
|
@ -455,7 +551,16 @@ FullLocationSchema = Annotated[
|
|||
Discriminator("locationtype"),
|
||||
]
|
||||
|
||||
SlimLocationSchema = Annotated[
|
||||
FullLocationSchema = Annotated[
|
||||
Union[
|
||||
FullListableLocationSchema,
|
||||
CustomLocationLocationSchema,
|
||||
TrackablePositionLocationSchema,
|
||||
],
|
||||
Discriminator("locationtype"),
|
||||
]
|
||||
|
||||
SlimListableLocationSchema = Annotated[
|
||||
Union[
|
||||
SlimLevelLocationSchema,
|
||||
SlimSpaceLocationSchema,
|
||||
|
@ -467,6 +572,15 @@ SlimLocationSchema = Annotated[
|
|||
Discriminator("locationtype"),
|
||||
]
|
||||
|
||||
SlimLocationSchema = Annotated[
|
||||
Union[
|
||||
SlimListableLocationSchema,
|
||||
CustomLocationLocationSchema,
|
||||
TrackablePositionLocationSchema,
|
||||
],
|
||||
Discriminator("locationtype"),
|
||||
]
|
||||
|
||||
|
||||
class DisplayLink(Schema):
|
||||
"""
|
||||
|
@ -479,7 +593,7 @@ class DisplayLink(Schema):
|
|||
|
||||
|
||||
class LocationDisplay(SerializableSchema):
|
||||
id: LocationID = APIField(
|
||||
id: AnyLocationID = APIField(
|
||||
description="a numeric ID for a map location or a string ID for generated locations",
|
||||
)
|
||||
level: Optional[PositiveInt] = APIField(
|
||||
|
@ -507,3 +621,29 @@ class LocationDisplay(SerializableSchema):
|
|||
editor_url: Optional[NonEmptyStr] = APIField(
|
||||
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_poi', self.near_poi.pk if self.near_poi else None),
|
||||
('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'):
|
||||
result['score'] = self.score
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue