add more mapdata API endpoints
This commit is contained in:
parent
f43d458fc4
commit
846101ea37
6 changed files with 300 additions and 17 deletions
|
@ -326,7 +326,7 @@ class LeaveDescription(SerializableMixin):
|
||||||
result = super()._serialize(**kwargs)
|
result = super()._serialize(**kwargs)
|
||||||
result['space'] = self.space_id
|
result['space'] = self.space_id
|
||||||
result['target_space'] = self.target_space_id
|
result['target_space'] = self.target_space_id
|
||||||
result['description_i18n'] = self.description_i18n
|
result['descriptions'] = self.description_i18n
|
||||||
result['description'] = self.description
|
result['description'] = self.description
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -366,7 +366,7 @@ class CrossDescription(SerializableMixin):
|
||||||
result['space'] = self.space_id
|
result['space'] = self.space_id
|
||||||
result['origin_space'] = self.origin_space_id
|
result['origin_space'] = self.origin_space_id
|
||||||
result['target_space'] = self.target_space_id
|
result['target_space'] = self.target_space_id
|
||||||
result['description_i18n'] = self.description_i18n
|
result['descriptions'] = self.description_i18n
|
||||||
result['description'] = self.description
|
result['description'] = self.description
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -293,9 +293,21 @@ class LocationGroupCategory(SerializableMixin, models.Model):
|
||||||
def _serialize(self, detailed=True, **kwargs):
|
def _serialize(self, detailed=True, **kwargs):
|
||||||
result = super()._serialize(detailed=detailed, **kwargs)
|
result = super()._serialize(detailed=detailed, **kwargs)
|
||||||
result['name'] = self.name
|
result['name'] = self.name
|
||||||
|
result['single'] = self.single
|
||||||
if detailed:
|
if detailed:
|
||||||
result['titles'] = self.titles
|
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
|
return result
|
||||||
|
|
||||||
def register_changed_geometries(self):
|
def register_changed_geometries(self):
|
||||||
|
@ -352,6 +364,9 @@ class LocationGroup(Location, models.Model):
|
||||||
def _serialize(self, simple_geometry=False, **kwargs):
|
def _serialize(self, simple_geometry=False, **kwargs):
|
||||||
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
|
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
|
||||||
result['category'] = self.category_id
|
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
|
result['color'] = self.color
|
||||||
if simple_geometry:
|
if simple_geometry:
|
||||||
result['locations'] = tuple(obj.pk for obj in getattr(self, 'locations', ()))
|
result['locations'] = tuple(obj.pk for obj in getattr(self, 'locations', ()))
|
||||||
|
|
|
@ -7,13 +7,17 @@ from ninja.pagination import paginate
|
||||||
|
|
||||||
from c3nav.api.exceptions import API404
|
from c3nav.api.exceptions import API404
|
||||||
from c3nav.mapdata.api import optimize_query
|
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.access import AccessPermission
|
||||||
from c3nav.mapdata.models.geometry.space import POI, Column, LineObstacle, Obstacle, Ramp
|
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
||||||
from c3nav.mapdata.schemas.filters import ByGroupFilter, ByLevelFilter, ByOnTopOfFilter, BySpaceFilter, FilterSchema
|
Obstacle, Ramp)
|
||||||
from c3nav.mapdata.schemas.models import (AreaSchema, BuildingSchema, ColumnSchema, DoorSchema, HoleSchema, LevelSchema,
|
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByLevelFilter, ByOnTopOfFilter,
|
||||||
LineObstacleSchema, ObstacleSchema, POISchema, RampSchema, SpaceSchema,
|
BySpaceFilter, FilterSchema)
|
||||||
StairSchema)
|
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"])
|
mapdata_api_router = APIRouter(tags=["mapdata"])
|
||||||
|
|
||||||
|
@ -316,3 +320,103 @@ def poi_list(request, filters: Query[BySpaceFilter]):
|
||||||
def poi_detail(request, poi_id: int):
|
def poi_detail(request, poi_id: int):
|
||||||
# todo: access, caching, filtering, etc
|
# todo: access, caching, filtering, etc
|
||||||
return mapdata_retrieve_endpoint(request, POI, pk=poi_id)
|
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)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from ninja import Schema
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
|
|
||||||
from c3nav.api.exceptions import APIRequestValidationFailed
|
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
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,6 +82,24 @@ class BySpaceFilter(FilterSchema):
|
||||||
return super().filter_qs(qs)
|
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):
|
class ByGroupFilter(FilterSchema):
|
||||||
group: Optional[int] = APIField(
|
group: Optional[int] = APIField(
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -116,6 +116,7 @@ class SpecificLocationSchema(LocationSchema):
|
||||||
label_settings: Optional[LabelSettingsSchema] = APIField(
|
label_settings: Optional[LabelSettingsSchema] = APIField(
|
||||||
default=None,
|
default=None,
|
||||||
title="label settings",
|
title="label settings",
|
||||||
|
description="if not set, it may be taken from location groups"
|
||||||
)
|
)
|
||||||
label_override: Optional[NonEmptyStr] = APIField(
|
label_override: Optional[NonEmptyStr] = APIField(
|
||||||
default=None,
|
default=None,
|
||||||
|
|
|
@ -2,10 +2,10 @@ from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
|
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
|
||||||
from pydantic.color import Color
|
|
||||||
|
|
||||||
from c3nav.api.utils import NonEmptyStr
|
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,
|
WithLevelSchema, WithLineStringGeometrySchema, WithPointGeometrySchema,
|
||||||
WithPolygonGeometrySchema, WithSpaceSchema)
|
WithPolygonGeometrySchema, WithSpaceSchema)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class LevelSchema(SpecificLocationSchema, DjangoModelSchema):
|
||||||
"""
|
"""
|
||||||
A physical level of the map, containing building, spaces, doors…
|
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(
|
short_label: NonEmptyStr = APIField(
|
||||||
title="short label (for level selector)",
|
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.
|
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(
|
outside: bool = APIField(
|
||||||
title="outside only",
|
title="outside only",
|
||||||
|
@ -76,7 +76,7 @@ class AreaSchema(WithPolygonGeometrySchema, SpecificLocationSchema, WithSpaceSch
|
||||||
"""
|
"""
|
||||||
An area inside a space.
|
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(
|
slow_down_factor: PositiveFloat = APIField(
|
||||||
title="slow-down factor",
|
title="slow-down factor",
|
||||||
|
@ -107,7 +107,7 @@ class BaseObstacleSchema(WithSpaceSchema, DjangoModelSchema):
|
||||||
title="altitude above ground",
|
title="altitude above ground",
|
||||||
description="altitude above ground"
|
description="altitude above ground"
|
||||||
)
|
)
|
||||||
color: Optional[Color] = APIField(
|
color: Optional[NonEmptyStr] = APIField(
|
||||||
title="color",
|
title="color",
|
||||||
description="an optional color for this obstacle"
|
description="an optional color for this obstacle"
|
||||||
)
|
)
|
||||||
|
@ -141,6 +141,151 @@ class POISchema(WithPointGeometrySchema, SpecificLocationSchema, WithSpaceSchema
|
||||||
"""
|
"""
|
||||||
A point of interest inside a space.
|
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
|
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue