more map api documentation improvements

This commit is contained in:
Laura Klünder 2023-12-04 18:58:49 +01:00
parent 4d57b81ad1
commit 964574e535
8 changed files with 335 additions and 121 deletions

View file

@ -22,8 +22,7 @@ class SwaggerAndRedoc(DocsBase):
redoc_config = Redoc(settings={
"hideOneOfDescription": True,
"expandSingleSchemaField": True,
"jsonSampleExpandLevel": 4,
"showObjectSchemaExamples": True,
"jsonSampleExpandLevel": 5,
"expandResponses": "200",
"hideSingleRequestSampleTab": True,
"nativeScrollbars": True,

View file

@ -25,6 +25,9 @@ class PolygonSchema(Schema):
example=[[[1.5, 1.5], [1.5, 2.5], [2.5, 2.5], [2.5, 2.5]]]
)
class Config(Schema.Config):
title = "GeoJSON Polygon"
class LineStringSchema(Schema):
"""
@ -35,6 +38,9 @@ class LineStringSchema(Schema):
example=[[1.5, 1.5], [2.5, 2.5], [5, 8.7]]
)
class Config(Schema.Config):
title = "GeoJSON LineString"
class LineSchema(Schema):
"""
@ -45,6 +51,9 @@ class LineSchema(Schema):
example=[[1.5, 1.5], [5, 8.7]]
)
class Config(Schema.Config):
title = "GeoJSON LineString (only two points)"
class PointSchema(Schema):
"""
@ -55,6 +64,9 @@ class PointSchema(Schema):
example=[1, 2.5]
)
class Config(Schema.Config):
title = "GeoJSON Point"
GeometrySchema = Annotated[
Union[

View file

@ -10,14 +10,14 @@ from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, Geome
from c3nav.editor.views.base import editor_etag_func
from c3nav.mapdata.api.base import api_etag
from c3nav.mapdata.models import Source
from c3nav.mapdata.schemas.responses import BoundsSchema
from c3nav.mapdata.schemas.responses import WithBoundsSchema
editor_api_router = APIRouter(tags=["editor"], auth=APITokenAuth(permissions={"editor_access"}))
@editor_api_router.get('/bounds/', summary="boundaries",
description="get maximum boundaries of everything on the map",
response={200: BoundsSchema, **auth_permission_responses},
response={200: WithBoundsSchema, **auth_permission_responses},
openapi_extra={"security": [{"APITokenAuth": ["editor_access"]}]})
@api_etag()
def bounds(request):

View file

@ -17,10 +17,11 @@ from c3nav.mapdata.api.base import api_etag, api_stats
from c3nav.mapdata.models import Source
from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID, schema_definition
from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema,
LocationDisplay, SlimListableLocationSchema, SlimLocationSchema)
from c3nav.mapdata.schemas.responses import BoundsSchema, LocationGeometry
LevelSchema, LocationDisplay, SlimListableLocationSchema, SlimLocationSchema,
all_location_definitions, listable_location_definitions)
from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema
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)
from c3nav.mapdata.utils.user import can_access_editor
@ -30,7 +31,7 @@ map_api_router = APIRouter(tags=["map"])
@map_api_router.get('/bounds/', summary="get boundaries",
description="get maximum boundaries of everything on the map",
response={200: BoundsSchema, **auth_responses})
response={200: WithBoundsSchema, **auth_responses})
@api_etag(permissions=False)
def bounds(request):
return {
@ -69,7 +70,8 @@ def _location_list(request, detailed: bool, filters: LocationListFilters):
@map_api_router.get('/locations/', summary="list locations (slim)",
description="Get locations (with most important attributes set)",
description=("Get locations (with most important attributes set)\n\n"
"Possible location types:\n"+listable_location_definitions),
response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses})
@api_etag(base_mapdata=True)
def location_list(request, filters: Query[LocationListFilters]):
@ -77,7 +79,8 @@ def location_list(request, filters: Query[LocationListFilters]):
@map_api_router.get('/locations/full/', summary="list locations (full)",
description="Get locations (with all attributes set)",
description=("Get locations (with all attributes set)\n\n"
"Possible location types:\n"+listable_location_definitions),
response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses})
@api_etag(base_mapdata=True)
def location_list_full(request, filters: Query[LocationListFilters]):
@ -150,7 +153,8 @@ class ShowRedirects(Schema):
@map_api_router.get('/locations/{location_id}/', summary="location by ID (slim)",
description="Get locations by ID (with all attributes set)",
description=("Get locations by ID (with all attributes set)\n\n"
"Possible location types:\n"+all_location_definitions),
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
@api_stats('location_get')
@api_etag(base_mapdata=True)
@ -164,7 +168,8 @@ def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeo
@map_api_router.get('/locations/{location_id}/full/', summary="location by ID (full)",
description="Get location by ID (with all attributes set)",
description=("Get location by ID (with all attributes set)\n\n"
"Possible location types:\n"+all_location_definitions),
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
@api_stats('location_get')
@api_etag(base_mapdata=True)
@ -202,7 +207,8 @@ def location_by_id_geometry(request, location_id: AnyLocationID):
@map_api_router.get('/locations/by-slug/{location_slug}/', summary="location by slug (slim)",
description="Get location by slug (with most important attributes set)",
description=("Get location by slug (with most important attributes set)\n\n"
"Possible location types:\n"+all_location_definitions),
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
@api_stats('location_get')
@api_etag(base_mapdata=True)
@ -216,7 +222,8 @@ def location_by_slug(request, location_slug: NonEmptyStr, filters: Query[RemoveG
@map_api_router.get('/locations/by-slug/{location_slug}/full/', summary="location by slug (full)",
description="Get location by slug (with all attributes set)",
description=("Get location by slug (with all attributes set)\n\n"
"Possible location types:\n"+all_location_definitions),
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
@api_stats('location_get')
@api_etag(base_mapdata=True)

View file

@ -15,6 +15,7 @@ from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription,
from c3nav.mapdata.models.locations import DynamicLocation
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema,
LevelGeometryFilter, SpaceGeometryFilter)
from c3nav.mapdata.schemas.model_base import schema_description
from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema,
BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema,
DynamicLocationSchema, HoleSchema, LeaveDescriptionSchema, LevelSchema,
@ -58,10 +59,6 @@ def mapdata_retrieve_endpoint(request, model: Type[Model], **lookups):
raise API404("%s not found" % model.__name__.lower())
def schema_description(schema):
return schema.__doc__.replace("\n ", "\n")
"""
Levels
"""

View file

@ -1,4 +1,5 @@
from typing import Annotated, Any, Optional, Union
import re
from typing import Annotated, Any, Union
from ninja import Schema
from pydantic import Field as APIField
@ -10,6 +11,31 @@ from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema
from c3nav.api.utils import NonEmptyStr
def schema_description(schema):
return schema.__doc__.replace("\n ", "\n").strip()
def schema_definition(schema):
return ("- **"+re.sub(r"([a-z])([A-Z])", r"\1 \2", schema.__name__.removesuffix("Schema")) +"**: " +
schema_description(schema).split("\n")[0].strip())
def schema_definitions(schemas):
return "\n".join(schema_definition(schema) for schema in schemas)
BoundsSchema = tuple[
Annotated[tuple[
Annotated[float, APIField(title="left", description="lowest X coordindate")],
Annotated[float, APIField(title="bottom", description="lowest Y coordindate")]
], APIField(title="(left, bottom)", description="lowest coordinates", example=(-10, -20))],
Annotated[tuple[
Annotated[float, APIField(title="right", description="highest X coordindate")],
Annotated[float, APIField(title="top", description="highest Y coordindate")]
], APIField(title="(right, top)", description="highest coordinates", example=(20, 30))]
]
class SerializableSchema(Schema):
@model_validator(mode="wrap") # noqa
@classmethod
@ -23,62 +49,79 @@ class SerializableSchema(Schema):
class DjangoModelSchema(SerializableSchema):
id: PositiveInt = APIField(
title="ID",
example=1,
)
class LocationSlugSchema(Schema):
slug: NonEmptyStr = APIField(
title="location slug",
description="a slug is a unique way to refer to a location across all location types. "
"locations can have a human-readable slug. "
"if it doesn't, this field holds a slug generated based from the location type and ID. "
"this slug will work even if a human-readable slug is defined later. "
"even dynamic locations like coordinates have a slug.",
description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs"
"are meants to be human-readable and easy to remember.\n\n"
"if a location doesn't have a slug defined, this field holds a slug generated from the "
"location type and ID, which will work even if a human-readable slug is defined later.\n\n"
"even dynamic locations like coordinates have an (auto-generated) slug.",
example="entrance",
)
class WithAccessRestrictionSchema(Schema):
access_restriction: Optional[PositiveInt] = APIField(
access_restriction: Union[
Annotated[PositiveInt, APIField(title="access restriction ID")],
Annotated[None, APIField(title="null", description="no access restriction")],
] = APIField(
default=None,
title="access restriction ID",
description="access restriction that this object belongs to",
)
class TitledSchema(Schema):
titles: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="title (all languages)",
description="property names are the ISO-language code. languages may be missing.",
description="title in all available languages. property names are the ISO-language code. "
"languages may be missing.",
example={
"en": "Title",
"de": "Titel",
"en": "Entrance",
"de": "Eingang",
}
)
title: NonEmptyStr = APIField(
title="title (preferred language)",
description="preferred language based on the Accept-Language header."
description="title in the preferred language based on the Accept-Language header.",
example="Entrance",
)
class LocationSchema(WithAccessRestrictionSchema, TitledSchema, LocationSlugSchema):
subtitle: NonEmptyStr = APIField(
title="subtitle (preferred language)",
description="an automatically generated short description for this location. "
"preferred language based on the Accept-Language header."
description="an automatically generated short description for this location in the "
"preferred language based on the Accept-Language header.",
example="near Area 51",
)
icon: Optional[NonEmptyStr] = APIField(
default=None,
icon: NonEmptyStr = APIField(
title="icon name",
description="any material design icon name"
description="any material design icon name",
example="pin_drop",
)
can_search: bool = APIField(
title="can be searched",
description="if `true`, this object can show up in search results",
)
can_describe: bool = APIField(
title="can describe locations",
description="if `true`, this object can be used to describe other locations (e.g. in their subtitle)",
)
add_search: Optional[str] = APIField(
add_search: Union[
Annotated[str, APIField(title="search terms", description="set when looking for searchable locations")],
Annotated[None, APIField(title="null", description="when not looking for searchable locations")],
] = APIField(
None,
title="more data for search index, only set when looking for searchable locations"
title="additional search terms",
description="more data for the search index separated by spaces, "
"only set when looking for searchable locations",
example="more search terms",
)
@ -90,49 +133,91 @@ class LabelSettingsSchema(DjangoModelSchema): # todo: add titles back in here
min_zoom: float = APIField(
-10,
title="min zoom",
description="minimum zoom to display the label at",
)
max_zoom: float = APIField(
10,
title="max zoom",
description="maximum, zoom to display the label at",
)
font_size: PositiveInt = APIField(
title="font size",
description="font size of the label",
example=12,
)
class SpecificLocationSchema(LocationSchema):
grid_square: Optional[NonEmptyStr] = APIField(
grid_square: Union[
Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")],
Annotated[None, APIField(title="null", description="no grid defined or outside of grid")],
] = APIField(
default=None,
title="grid square",
description="if a grid is defined and this location is within it",
description="grid cell(s) that this location is in, if a grid is defined and the location is within it",
example="C3",
)
groups: dict[NonEmptyStr, list[PositiveInt] | Optional[PositiveInt]] = APIField(
groups: dict[
Annotated[NonEmptyStr, APIField(title="location group category name")],
Union[
Annotated[list[PositiveInt], APIField(
title="array of location IDs",
description="for categories that have `single` set to `false`. can be an empty array.",
example=[1,4,5],
)],
Annotated[PositiveInt, APIField(
title="one location ID",
description="for categories that have `single` set to `true`.",
example=1,
)],
Annotated[None, APIField(
title="null",
description="for categories that have `single` set to `true`."
)],
]
] = APIField(
title="location groups",
description="grouped by location group categories. "
"property names are the names of location groupes. "
"property values are integer, None or a list of integers, see example."
"see location group category endpoint for currently available possibilities."
description="location group(s) that this specific location belongs to, grouped by categories.\n\n"
"keys are location group category names. see location group category endpoint for details.\n\n"
"categories may be missing if no groups apply.",
example={
"category_with_single_true": 5,
"other_category_with_single_true": None,
"categoryother_category_with_single_false": [1, 3, 7],
"category_with_single_false": [1, 3, 7],
}
)
label_settings: Optional[LabelSettingsSchema] = APIField(
label_settings: Union[
Annotated[LabelSettingsSchema, APIField(
title="label settings",
description="label settings to use",
)],
Annotated[None, APIField(
title="null",
description="label settings from location group will be used, if available"
)],
] = APIField(
default=None,
title="label settings",
description="if not set, it may be taken from location groups"
description=(
schema_description(LabelSettingsSchema) +
"\n\nif not set, label settings of location groups might be used"
)
label_override: Optional[NonEmptyStr] = APIField(
)
label_override: Union[
Annotated[NonEmptyStr, APIField(title="label override", description="text to use for label")],
Annotated[None, APIField(title="null", description="title will be used")],
] = APIField(
default=None,
title="label override (preferred language)",
description="preferred language based on the Accept-Language header."
description="text to use for the label. by default (null), the title would be used."
)
class WithPolygonGeometrySchema(Schema):
geometry: Optional[PolygonSchema] = APIField(
geometry: Union[
PolygonSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
@ -140,7 +225,10 @@ class WithPolygonGeometrySchema(Schema):
class WithLineStringGeometrySchema(Schema):
geometry: Optional[LineStringSchema] = APIField(
geometry: Union[
LineStringSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
@ -148,7 +236,10 @@ class WithLineStringGeometrySchema(Schema):
class WithPointGeometrySchema(Schema):
geometry: Optional[PointSchema] = APIField(
geometry: Union[
PointSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
@ -159,6 +250,7 @@ class WithLevelSchema(SerializableSchema):
level: PositiveInt = APIField(
title="level",
description="level id this object belongs to.",
example=1,
)
@ -166,6 +258,7 @@ class WithSpaceSchema(SerializableSchema):
space: PositiveInt = APIField(
title="space",
description="space id this object belongs to.",
example=1,
)
@ -182,8 +275,8 @@ class SimpleGeometryPointSchema(Schema):
class SimpleGeometryPointAndBoundsSchema(SimpleGeometryPointSchema):
bounds: tuple[tuple[float, float], tuple[float, float]] = APIField(
description="location bounding box from (x, y) to (x, y)",
bounds: BoundsSchema = APIField(
description="location bounding box",
example=((-10, -20), (20, 30)),
)
@ -212,7 +305,7 @@ Coordinates3D = tuple[float, float, float]
AnyLocationID = Union[
Annotated[PositiveInt, APIField(
title="location ID",
description="numeric ID of any lcation"
description="numeric ID of any lcation all locations have a shared ID space"
)],
CustomLocationID,
PositionID,

View file

@ -13,7 +13,8 @@ from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, Cust
SimpleGeometryPointSchema, SpecificLocationSchema, TitledSchema,
WithAccessRestrictionSchema, WithLevelSchema,
WithLineStringGeometrySchema, WithPointGeometrySchema,
WithPolygonGeometrySchema, WithSpaceSchema)
WithPolygonGeometrySchema, WithSpaceSchema, schema_definitions,
schema_description)
class LevelSchema(SpecificLocationSchema, DjangoModelSchema):
@ -26,18 +27,26 @@ class LevelSchema(SpecificLocationSchema, DjangoModelSchema):
title="short label (for level selector)",
description="unique among levels",
)
on_top_of: Optional[PositiveInt] = APIField(
on_top_of: Union[
Annotated[PositiveInt, APIField(title="level ID", description="level this level is on top of", example=1)],
Annotated[None, APIField(title="null", description="this is a main level, not on top of any other")]
] = APIField(
title="on top of level ID",
description="if set, this is not a main level, but it's on top of this other level"
)
base_altitude: float = APIField(
title="base/default altitude",
description="default ground altitude for this level, if it can't be determined using altitude markers.",
)
default_height: PositiveFloat = APIField(
title="default ceiling height",
description="default ceiling height for all spaces that don't set their own",
example=2.5
)
door_height: PositiveFloat = APIField(
title="door height",
description="height for all doors on this level",
example="2.0",
)
@ -86,7 +95,8 @@ class AreaSchema(WithPolygonGeometrySchema, SpecificLocationSchema, WithSpaceSch
"""
slow_down_factor: PositiveFloat = APIField(
title="slow-down factor",
description="how much walking in this area is slowed down, overlapping areas are multiplied"
description="how much walking in this area is slowed down, overlapping areas are multiplied",
example=1.0,
)
@ -212,16 +222,31 @@ class LocationGroupSchema(LocationSchema, DjangoModelSchema):
)
priority: int = APIField() # todo: ???
hierarchy: int = APIField() # todo: ???
label_settings: Optional[LabelSettingsSchema] = APIField(
label_settings: Union[
Annotated[LabelSettingsSchema, APIField(
title="label settings",
description="label settings to use for gruop members that don't have their own set",
)],
Annotated[None, APIField(
title="null",
description="no label settings set"
)],
] = APIField(
default=None,
title="label settings",
description="for locations with this group, can be overwritten by specific locations"
description=(
schema_description(LabelSettingsSchema) +
"\n\nlocations can override this setting"
)
)
can_report_missing: bool = APIField(
title="report missing locations",
description="can be used in form for reporting missing locations",
)
color: Optional[NonEmptyStr] = APIField(
color: Union[
Annotated[NonEmptyStr, APIField(title="color", description="a valid CSS color expression")],
Annotated[None, APIField(title="null", description="default/no color will be used")],
] = APIField(
title="color",
description="an optional color for spaces and areas with this group"
)
@ -285,7 +310,7 @@ class LocationGroupCategorySchema(TitledSchema, DjangoModelSchema):
class DynamicLocationSchema(SpecificLocationSchema, DjangoModelSchema):
"""
A dynamic location represents a moving object. Its position has to be separately queries through the position API.
Represents a moving object. Its position has to be separately queried through the position API.
A dynamic location is a specific location, and can therefore be routed to and from,
as well as belong to location groups.
@ -344,10 +369,10 @@ class CustomLocationSchema(SerializableSchema):
slug: CustomLocationID = APIField(
description="slug, identical to ID"
)
icon: Optional[NonEmptyStr] = APIField(
default=None,
icon: NonEmptyStr = APIField(
title="icon name",
description="any material design icon name"
description="any material design icon name",
example="pin_drop",
)
title: NonEmptyStr = APIField(
title="title (preferred language)",
@ -359,32 +384,56 @@ class CustomLocationSchema(SerializableSchema):
"preferred language based on the Accept-Language header."
)
level: PositiveInt = APIField(
description="level ID this custom location is located on"
description="level ID this custom location is located on",
example=1,
)
space: Optional[PositiveInt] = APIField(
description="space ID this custom location is located in, if applicable"
space: Union[
Annotated[PositiveInt, APIField(title="space ID", example=1)],
Annotated[None, APIField(title="null", description="the location is not inside a space")],
] = APIField(
default=None,
description="space ID this custom location is located in"
)
areas: list[PositiveInt] = APIField(
description="IDs of areas this custom location is located in"
)
grid_square: Optional[NonEmptyStr] = APIField(
grid_square: Union[
Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")],
Annotated[None, APIField(title="null", description="no grid defined or outside of grid")],
] = APIField(
default=None,
title="grid square",
description="if a grid is defined and this custom location is within it",
description="grid cell(s) that this location is in, if a grid is defined and the location is within it",
example="C3",
)
near_area: Optional[PositiveInt] = APIField(
description="the ID of an area near this custom location, if there is one"
near_area: Union[
Annotated[PositiveInt, APIField(title="area ID", example=1)],
Annotated[None, APIField(title="null", description="the location is not near any areas")],
] = APIField(
near="nearby area",
description="the ID of an area near this custom location"
)
near_poi: Optional[PositiveInt] = APIField(
description="the ID of a POI near this custom location, if there is one"
near_poi: Union[
Annotated[PositiveInt, APIField(title="POI ID", example=1)],
Annotated[None, APIField(title="null", description="the location is not near any POIs")],
] = APIField(
title="nearby POI",
description="the ID of a POI near this custom location"
)
nearby: 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"
altitude: Union[
Annotated[float, APIField(title="ground altitude", example=1)],
Annotated[None, APIField(title="null", description="could not be determined (outside of space?)")],
] = APIField(
title="ground altitude",
description="ground altitude (in the map-wide coordinate system)"
)
geometry: Optional[PointSchema] = APIField(
geometry: Union[
PointSchema,
Annotated[None, APIField(title="null", description="geometry excluded from endpoint")]
] = APIField(
None,
description="point geometry for this custom location",
)
@ -392,37 +441,43 @@ class CustomLocationSchema(SerializableSchema):
class TrackablePositionSchema(Schema):
"""
A trackable position. It's position can be set or reset.
A trackable position. Its position can be set or reset.
"""
id: PositionID = APIField(
description="ID representing the position"
description="ID representing the position",
example="p:adskjfalskdj",
)
slug: PositionID = APIField(
description="slug representing the position"
description="slug representing the position",
example="p:adskjfalskdj",
)
icon: Optional[NonEmptyStr] = APIField(
default=None,
icon: NonEmptyStr = APIField(
title="icon name",
description="any material design icon name"
description="any material design icon name",
example="pin_drop",
)
title: NonEmptyStr = APIField(
title="title of the position",
example="My 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."
"preferred language based on the Accept-Language header.",
example="Near Bällebad"
)
def put_locationtype_first(schema):
fields = schema.__fields__.copy()
schema.__fields__ = {"locationtype": fields.pop("locationtype"), **fields}
return schema
class LocationTypeSchema(Schema):
locationtype: str
locationtype: str = APIField(title="location type",
description="indicates what kind of location is included. "
"different location types have different fields.")
def LocationTypeAPIField():
return APIField(title="location type",
description="indicates what kind of location is included. "
"different location types have different fields.")
class FullLevelLocationSchema(LevelSchema, LocationTypeSchema):
@ -430,7 +485,7 @@ class FullLevelLocationSchema(LevelSchema, LocationTypeSchema):
A level for the location API.
See Level schema for details.
"""
locationtype: Literal["level"]
locationtype: Literal["level"] = LocationTypeAPIField()
class FullSpaceLocationSchema(SimpleGeometryPointAndBoundsSchema, SpaceSchema, LocationTypeSchema):
@ -438,7 +493,7 @@ class FullSpaceLocationSchema(SimpleGeometryPointAndBoundsSchema, SpaceSchema, L
A space with some additional information for the location API.
See Space schema for details.
"""
locationtype: Literal["space"]
locationtype: Literal["space"] = LocationTypeAPIField()
class FullAreaLocationSchema(SimpleGeometryPointAndBoundsSchema, AreaSchema, LocationTypeSchema):
@ -446,7 +501,7 @@ class FullAreaLocationSchema(SimpleGeometryPointAndBoundsSchema, AreaSchema, Loc
An area with some additional information for the location API.
See Area schema for details.
"""
locationtype: Literal["area"]
locationtype: Literal["area"] = LocationTypeAPIField()
class FullPOILocationSchema(SimpleGeometryPointSchema, POISchema, LocationTypeSchema):
@ -454,7 +509,7 @@ class FullPOILocationSchema(SimpleGeometryPointSchema, POISchema, LocationTypeSc
A point of interest with some additional information for the location API.
See POI schema for details.
"""
locationtype: Literal["poi"]
locationtype: Literal["poi"] = LocationTypeAPIField()
class FullLocationGroupLocationSchema(SimpleGeometryLocationsSchema, LocationGroupSchema, LocationTypeSchema):
@ -462,7 +517,7 @@ class FullLocationGroupLocationSchema(SimpleGeometryLocationsSchema, LocationGro
A location group with some additional information for the location API.
See LocationGroup schema for details.
"""
locationtype: Literal["locationgroup"]
locationtype: Literal["locationgroup"] = LocationTypeAPIField()
class FullDynamicLocationLocationSchema(DynamicLocationSchema, LocationTypeSchema):
@ -470,7 +525,7 @@ class FullDynamicLocationLocationSchema(DynamicLocationSchema, LocationTypeSchem
A dynamic location for the location API.
See DynamicLocation schema for details.
"""
locationtype: Literal["dynamiclocation"]
locationtype: Literal["dynamiclocation"] = LocationTypeAPIField()
class CustomLocationLocationSchema(SimpleGeometryPointAndBoundsSchema, CustomLocationSchema, LocationTypeSchema):
@ -478,7 +533,7 @@ class CustomLocationLocationSchema(SimpleGeometryPointAndBoundsSchema, CustomLoc
A custom location for the location API.
See CustomLocation schema for details.
"""
locationtype: Literal["customlocation"]
locationtype: Literal["customlocation"] = LocationTypeAPIField()
class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchema):
@ -486,7 +541,7 @@ class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchem
A trackable position for the location API.
See TrackablePosition schema for details.
"""
locationtype: Literal["position"]
locationtype: Literal["position"] = LocationTypeAPIField()
class SlimLocationMixin(Schema):
@ -599,6 +654,14 @@ SlimLocationSchema = Annotated[
]
listable_location_definitions = schema_definitions(
(LevelSchema, SpaceSchema, AreaSchema, POISchema, DynamicLocationSchema, LocationGroupSchema)
)
all_location_definitions = listable_location_definitions + "\n" + schema_definitions(
(CustomLocationSchema, TrackablePositionSchema)
)
class DisplayLink(Schema):
"""
A link for the location display
@ -612,14 +675,24 @@ class DisplayLink(Schema):
class LocationDisplay(SerializableSchema):
id: AnyLocationID = APIField(
description="a numeric ID for a map location or a string ID for generated locations",
example=1,
)
level: Optional[PositiveInt] = APIField(
level: Union[
Annotated[PositiveInt, APIField(title="level ID", description="ID of relevant level")],
Annotated[None, APIField(title="null", description="no relevant level")],
] = APIField(
None,
description="level ID, if applicable"
title="level",
example=2,
)
space: Optional[PositiveInt] = APIField(
space: Union[
Annotated[PositiveInt, APIField(title="level ID", description="ID of relevant level")],
Annotated[None, APIField(title="null", description="no relevant level")],
] = APIField(
None,
description="space ID, if applicable"
description="space",
example=3,
)
display: list[
tuple[
@ -631,12 +704,48 @@ class LocationDisplay(SerializableSchema):
Annotated[None, APIField(title="no value")]
], APIField(title="field value", union_mode='left_to_right')]
]
] = APIField(description="a list of human-readable display values")
geometry: Optional[GeometrySchema] = APIField(
] = APIField(
title="display fields",
description="a list of human-readable display values",
example=[
("Title", "Awesome location"),
("Access Restriction", None),
("Level", {
"id": 2,
"slug": "level0",
"title": "Ground Floor",
"can_search": True,
}),
("Groups", [
{
"id": 10,
"slug": "entrances",
"title": "Entrances",
"can_search": True,
},
{
"id": 11,
"slug": "startswithe",
"title": "Locations that Start with E",
"can_search": False,
}
])
]
)
geometry: Union[
GeometrySchema,
Annotated[None, APIField(title="null", description="no geometry available")]
] = 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",
editor_url: Union[
Annotated[NonEmptyStr, APIField(title="path to editor")],
Annotated[None, APIField(title="null", description="no editor access or object is not editable")],
] = APIField(
None,
title="editor URL",
description="path to edit this object in the editor",
example="/editor/spaces/2/pois/1/"
)

View file

@ -1,27 +1,18 @@
from typing import Annotated, Optional
from typing import Annotated, Union
from ninja import Schema
from pydantic import Field as APIField
from pydantic import PositiveInt
from c3nav.api.schema import GeometrySchema
from c3nav.mapdata.schemas.model_base import AnyLocationID
from c3nav.mapdata.schemas.model_base import AnyLocationID, BoundsSchema
class BoundsSchema(Schema):
class WithBoundsSchema(Schema):
"""
Describing a bounding box
"""
bounds: tuple[
Annotated[tuple[
Annotated[float, APIField(title="left", description="lowest X coordindate")],
Annotated[float, APIField(title="bottom", description="lowest Y coordindate")]
], APIField(title="(left, bottom)", description="lowest coordinates", example=(-10, -20))],
Annotated[tuple[
Annotated[float, APIField(title="right", description="highest X coordindate")],
Annotated[float, APIField(title="top", description="highest Y coordindate")]
], APIField(title="(right, top)", description="highest coordinates", example=(20, 30))]
] = APIField(
bounds: BoundsSchema = APIField(
title="boundaries",
description="(left, bottom) to (top, right)",
)
@ -31,9 +22,15 @@ class LocationGeometry(Schema):
id: AnyLocationID = APIField(
description="ID of the location that the geometry is being queried for",
)
level: Optional[PositiveInt] = APIField(
level: Union[
Annotated[PositiveInt, APIField(title="level ID")],
Annotated[None, APIField(title="null", description="geometry is not on any level")], # todo: possible?
] = APIField(
description="ID of the level the geometry is on",
)
geometry: Optional[GeometrySchema] = APIField(
geometry: Union[
GeometrySchema,
Annotated[None, APIField(title="null", description="no geometry available")]
] = APIField(
description="geometry, if available"
)