add more mapdata API endpoints

This commit is contained in:
Laura Klünder 2023-11-19 16:36:46 +01:00
parent f43d458fc4
commit 846101ea37
6 changed files with 300 additions and 17 deletions

View file

@ -326,7 +326,7 @@ class LeaveDescription(SerializableMixin):
result = super()._serialize(**kwargs)
result['space'] = self.space_id
result['target_space'] = self.target_space_id
result['description_i18n'] = self.description_i18n
result['descriptions'] = self.description_i18n
result['description'] = self.description
return result
@ -366,7 +366,7 @@ class CrossDescription(SerializableMixin):
result['space'] = self.space_id
result['origin_space'] = self.origin_space_id
result['target_space'] = self.target_space_id
result['description_i18n'] = self.description_i18n
result['descriptions'] = self.description_i18n
result['description'] = self.description
return result

View file

@ -293,9 +293,21 @@ class LocationGroupCategory(SerializableMixin, models.Model):
def _serialize(self, detailed=True, **kwargs):
result = super()._serialize(detailed=detailed, **kwargs)
result['name'] = self.name
result['single'] = self.single
if detailed:
result['titles'] = self.titles
result['title'] = self.title
result['titles_plural'] = self.titles_plural
result['help_texts'] = self.help_texts
result['title'] = str(self.title)
result['title_plural'] = str(self.title_plural)
result['help_text'] = str(self.help_text)
result['allow_levels'] = self.allow_levels
result['allow_spaces'] = self.allow_spaces
result['allow_areas'] = self.allow_areas
result['allow_pois'] = self.allow_pois
result['allow_dynamic_locations'] = self.allow_dynamic_locations
result['priority'] = self.priority
return result
def register_changed_geometries(self):
@ -352,6 +364,9 @@ class LocationGroup(Location, models.Model):
def _serialize(self, simple_geometry=False, **kwargs):
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
result['category'] = self.category_id
result['priority'] = self.priority
result['hierarchy'] = self.hierarchy
result['can_report_missing'] = self.can_report_missing
result['color'] = self.color
if simple_geometry:
result['locations'] = tuple(obj.pk for obj in getattr(self, 'locations', ()))

View file

@ -7,13 +7,17 @@ from ninja.pagination import paginate
from c3nav.api.exceptions import API404
from c3nav.mapdata.api import optimize_query
from c3nav.mapdata.models import Area, Building, Door, Hole, Level, Space, Stair
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
Space, Stair)
from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.models.geometry.space import POI, Column, LineObstacle, Obstacle, Ramp
from c3nav.mapdata.schemas.filters import ByGroupFilter, ByLevelFilter, ByOnTopOfFilter, BySpaceFilter, FilterSchema
from c3nav.mapdata.schemas.models import (AreaSchema, BuildingSchema, ColumnSchema, DoorSchema, HoleSchema, LevelSchema,
LineObstacleSchema, ObstacleSchema, POISchema, RampSchema, SpaceSchema,
StairSchema)
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
Obstacle, Ramp)
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByLevelFilter, ByOnTopOfFilter,
BySpaceFilter, FilterSchema)
from c3nav.mapdata.schemas.models import (AreaSchema, BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema,
HoleSchema, LeaveDescriptionSchema, LevelSchema, LineObstacleSchema,
LocationGroupCategorySchema, LocationGroupSchema, ObstacleSchema, POISchema,
RampSchema, SourceSchema, SpaceSchema, StairSchema)
mapdata_api_router = APIRouter(tags=["mapdata"])
@ -316,3 +320,103 @@ def poi_list(request, filters: Query[BySpaceFilter]):
def poi_detail(request, poi_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, POI, pk=poi_id)
"""
LeaveDescriptions
"""
@mapdata_api_router.get('/leavedescriptions/', response=list[LeaveDescriptionSchema],
summary="Get leave description list")
@paginate
def leavedescription_list(request, filters: Query[BySpaceFilter]):
# todo cache?
return mapdata_list_endpoint(request, model=LeaveDescription, filters=filters)
@mapdata_api_router.get('/leavedescriptions/{leavedescription_id}/', response=LeaveDescriptionSchema,
summary="Get leave description by ID")
def leavedescription_detail(request, leavedescription_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, LeaveDescription, pk=leavedescription_id)
"""
CrossDescriptions
"""
@mapdata_api_router.get('/crossdescriptions/', response=list[CrossDescriptionSchema],
summary="Get cross description list")
@paginate
def crossdescription_list(request, filters: Query[BySpaceFilter]):
# todo cache?
return mapdata_list_endpoint(request, model=CrossDescription, filters=filters)
@mapdata_api_router.get('/crossdescriptions/{crossdescription_id}/', response=CrossDescriptionSchema,
summary="Get cross description by ID")
def crossdescription_detail(request, crossdescription_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, CrossDescription, pk=crossdescription_id)
"""
LocationGroup
"""
@mapdata_api_router.get('/locationgroups/', response=list[LocationGroupSchema],
summary="Get location group list")
@paginate
def locationgroup_list(request, filters: Query[ByCategoryFilter]):
# todo cache?
return mapdata_list_endpoint(request, model=LocationGroup, filters=filters)
@mapdata_api_router.get('/locationgroups/{locationgroup_id}/', response=LocationGroupSchema,
summary="Get location group by ID")
def locationgroup_detail(request, locationgroup_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, LocationGroup, pk=locationgroup_id)
"""
LocationGroupCategories
"""
@mapdata_api_router.get('/locationgroupcategories/', response=list[LocationGroupCategorySchema],
summary="Get location group category list")
@paginate
def locationgroupcategory_list(request):
# todo cache?
return mapdata_list_endpoint(request, model=LocationGroupCategory)
@mapdata_api_router.get('/locationgroupcategories/{category_id}/', response=LocationGroupCategorySchema,
summary="Get location group category by ID")
def locationgroupcategory_detail(request, category_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, LocationGroupCategory, pk=category_id)
"""
Sources
"""
@mapdata_api_router.get('/sources/', response=list[SourceSchema],
summary="Get source list")
@paginate
def source_list(request):
# todo cache?
return mapdata_list_endpoint(request, model=Source)
@mapdata_api_router.get('/sources/{source_id}/', response=SourceSchema,
summary="Get source by ID")
def source_detail(request, source_id: int):
# todo: access, caching, filtering, etc
return mapdata_retrieve_endpoint(request, Source, pk=source_id)

View file

@ -6,7 +6,7 @@ from ninja import Schema
from pydantic import Field as APIField
from c3nav.api.exceptions import APIRequestValidationFailed
from c3nav.mapdata.models import Level, LocationGroup, MapUpdate, Space
from c3nav.mapdata.models import Level, LocationGroup, LocationGroupCategory, MapUpdate, Space
from c3nav.mapdata.models.access import AccessPermission
@ -82,6 +82,24 @@ class BySpaceFilter(FilterSchema):
return super().filter_qs(qs)
class ByCategoryFilter(FilterSchema):
category: Optional[int] = APIField(
None,
title="filter by location group category",
description="if set, only groups belonging to the location group category with this ID will be shown"
)
def validate(self, request):
super().validate(request)
if self.category is not None:
assert_valid_value(request, LocationGroupCategory, "pk", {self.category})
def filter_qs(self, qs: QuerySet) -> QuerySet:
if self.category is not None:
qs = qs.filter(category=self.category)
return super().filter_qs(qs)
class ByGroupFilter(FilterSchema):
group: Optional[int] = APIField(
None,

View file

@ -116,6 +116,7 @@ class SpecificLocationSchema(LocationSchema):
label_settings: Optional[LabelSettingsSchema] = APIField(
default=None,
title="label settings",
description="if not set, it may be taken from location groups"
)
label_override: Optional[NonEmptyStr] = APIField(
default=None,

View file

@ -2,10 +2,10 @@ from typing import Optional
from pydantic import Field as APIField
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
from pydantic.color import Color
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.schemas.model_base import (AccessRestrictionSchema, DjangoModelSchema, SpecificLocationSchema,
from c3nav.mapdata.schemas.model_base import (AccessRestrictionSchema, DjangoModelSchema, LabelSettingsSchema,
LocationSchema, SerializableSchema, SpecificLocationSchema, TitledSchema,
WithLevelSchema, WithLineStringGeometrySchema, WithPointGeometrySchema,
WithPolygonGeometrySchema, WithSpaceSchema)
@ -14,7 +14,7 @@ class LevelSchema(SpecificLocationSchema, DjangoModelSchema):
"""
A physical level of the map, containing building, spaces, doors
A level is a specific location, and can therefor be routed to and from, as well as belong to location groups.
A level is a specific location, and can therefore be routed to and from, as well as belong to location groups.
"""
short_label: NonEmptyStr = APIField(
title="short label (for level selector)",
@ -46,7 +46,7 @@ class SpaceSchema(WithPolygonGeometrySchema, SpecificLocationSchema, WithLevelSc
"""
An accessible area on a level. It can be outside-only or inside-only.
A space is a specific location, and can therefor be routed to and from, as well as belong to location groups.
A space is a specific location, and can therefore be routed to and from, as well as belong to location groups.
"""
outside: bool = APIField(
title="outside only",
@ -76,7 +76,7 @@ class AreaSchema(WithPolygonGeometrySchema, SpecificLocationSchema, WithSpaceSch
"""
An area inside a space.
An area is a specific location, and can therefor be routed to and from, as well as belong to location groups.
An area is a specific location, and can therefore be routed to and from, as well as belong to location groups.
"""
slow_down_factor: PositiveFloat = APIField(
title="slow-down factor",
@ -107,7 +107,7 @@ class BaseObstacleSchema(WithSpaceSchema, DjangoModelSchema):
title="altitude above ground",
description="altitude above ground"
)
color: Optional[Color] = APIField(
color: Optional[NonEmptyStr] = APIField(
title="color",
description="an optional color for this obstacle"
)
@ -141,6 +141,151 @@ class POISchema(WithPointGeometrySchema, SpecificLocationSchema, WithSpaceSchema
"""
A point of interest inside a space.
A POI is a specific location, and can therefor be routed to and from, as well as belong to location groups.
A POI is a specific location, and can therefore be routed to and from, as well as belong to location groups.
"""
pass
class LeaveDescriptionSchema(WithSpaceSchema, DjangoModelSchema):
"""
A description for leaving a space to enter another space.
"""
target_space: PositiveInt = APIField(
title="target space",
description="the space that is being entered",
)
descriptions: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="description (all languages)",
description="property names are the ISO-language code. languages may be missing.",
example={
"en": "Stanley walked through the red door.",
"de": "Stanley ging durch die rote Tür.",
}
)
description: NonEmptyStr = APIField(
title="description (preferred language)",
description="preferred language based on the Accept-Language header."
)
class CrossDescriptionSchema(WithSpaceSchema, DjangoModelSchema):
"""
A description for crossing through a space from one space to another.
"""
origin_space: PositiveInt = APIField(
title="origin space",
description="the space from which the main space is being entered",
)
target_space: PositiveInt = APIField(
title="target space",
description="the space that is being entered from the main space",
)
descriptions: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="description (all languages)",
description="property names are the ISO-language code. languages may be missing.",
example={
"en": "Go straight ahead through the big glass doors.",
"de": "gehe geradeaus durch die Glastüren.",
}
)
description: NonEmptyStr = APIField(
title="description (preferred language)",
description="preferred language based on the Accept-Language header."
)
class LocationGroupSchema(LocationSchema, DjangoModelSchema):
"""
A location group, always belonging to a location group category.
A location group is a (non-specific) location, which means it can be routed to and from.
"""
category: PositiveInt = APIField(
title="category",
description="location group category that this location group belongs to",
)
priority: int = APIField() # todo: ???
hierarchy: int = APIField() # todo: ???
label_settings: Optional[LabelSettingsSchema] = APIField(
default=None,
title="label settings",
description="for locations with this group, can be overwritten by specific locations"
)
can_report_missing: bool = APIField(
title="report missing locations",
description="can be used in form for reporting missing locations",
)
color: Optional[NonEmptyStr] = APIField(
title="color",
description="an optional color for spaces and areas with this group"
)
class LocationGroupCategorySchema(TitledSchema, SerializableSchema, DjangoModelSchema):
"""
A location group category can hold either one or multiple location groups.
It is used to allow for having different kind of groups for different means.
"""
name: NonEmptyStr = APIField(
title="name/slug",
description="name/slug of this location group category",
)
single: bool = APIField(
title="single choice",
description="if true, every location can only have one group from this category, not a list"
)
titles_plural: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="plural title (all languages)",
description="property names are the ISO-language code. languages may be missing.",
example={
"en": "Title",
"de": "Titel",
}
)
title_plural: NonEmptyStr = APIField(
title="plural title (preferred language)",
description="preferred language based on the Accept-Language header."
)
help_texts: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="help text (all languages)",
description="property names are the ISO-language code. languages may be missing.",
example={
"en": "Title",
"de": "Titel",
}
)
help_text: str = APIField(
title="help text (preferred language)",
description="preferred language based on the Accept-Language header."
)
allow_levels: bool = APIField(
description="whether groups with this category can be assigned to levels"
)
allow_spaces: bool = APIField(
description="whether groups with this category can be assigned to spaces"
)
allow_areas: bool = APIField(
description="whether groups with this category can be assigned to areas"
)
allow_pois: bool = APIField(
description="whether groups with this category can be assigned to POIs"
)
allow_dynamic_locations: bool = APIField(
description="whether groups with this category can be assigned to dynamic locations"
)
priority: int = APIField() # todo: ???
class SourceSchema(AccessRestrictionSchema, DjangoModelSchema):
"""
A source image that can be traced in the editor.
"""
name: NonEmptyStr = APIField(
title="name",
description="name/filename of the source",
)
bottom: float
left: float
top: float
right: float