convert django lazy string proxies to strings in the base schema validator, rather than in the serialization code of each source model

This commit is contained in:
Gwendolyn 2023-12-11 20:49:50 +01:00
parent 4c06abd400
commit 4b1ac9f194
21 changed files with 126 additions and 104 deletions

View file

@ -1,16 +1,16 @@
from django.conf import settings
from ninja import Field as APIField
from ninja import Router as APIRouter
from ninja import Schema
from c3nav.api.auth import APIKeyType, auth_responses
from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.control.models import UserPermissions
auth_api_router = APIRouter(tags=["auth"])
class AuthStatusSchema(Schema):
class AuthStatusSchema(BaseSchema):
"""
Current auth state and permissions
"""
@ -44,7 +44,7 @@ def get_status(request):
)
class APIKeySchema(Schema):
class APIKeySchema(BaseSchema):
key: NonEmptyStr = APIField(
title="API key",
description="API secret to be directly used with `X-API-Key` HTTP header."

View file

@ -1,3 +1,5 @@
from django.utils.functional import Promise
from c3nav.api.schema import APIErrorSchema
@ -7,7 +9,10 @@ class CustomAPIException(Exception):
def __init__(self, detail=None):
if detail is not None:
self.detail = detail
if isinstance(detail, Promise):
self.detail = str(detail)
else:
self.detail = detail
def get_response(self, api, request):
return api.create_response(request, {"detail": self.detail}, status=self.status_code)

View file

@ -1,13 +1,44 @@
from types import NoneType
from typing import Annotated, Any, Literal, Union
from django.utils.functional import Promise
from ninja import Schema
from pydantic import Discriminator
from pydantic import Discriminator, model_validator
from pydantic import Field as APIField
from pydantic.functional_validators import ModelWrapValidatorHandler
from pydantic_core.core_schema import ValidationInfo
from c3nav.api.utils import NonEmptyStr
class APIErrorSchema(Schema):
class BaseSchema(Schema):
@model_validator(mode="wrap") # noqa
@classmethod
def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any:
""" overwriting this, we need to call serialize to get the correct data """
return handler(cls.convert(values))
@classmethod
def convert(cls, values: Any):
if isinstance(values, Schema):
return values
if isinstance(values, (str, bool, int, float, complex, NoneType)):
return values
if isinstance(values, dict):
return {
key: cls.convert(val)
for key, val in values.items()
}
if isinstance(values, (list, tuple, set, frozenset)):
return type(values)(cls.convert(val) for val in values)
if isinstance(values, Promise):
return str(values)
if hasattr(values, 'serialize') and callable(values.serialize):
return values.serialize()
return values
class APIErrorSchema(BaseSchema):
"""
An error has occured with this request
"""
@ -16,7 +47,7 @@ class APIErrorSchema(Schema):
)
class PolygonSchema(Schema):
class PolygonSchema(BaseSchema):
"""
A GeoJSON Polygon
"""
@ -29,7 +60,7 @@ class PolygonSchema(Schema):
title = "GeoJSON Polygon"
class LineStringSchema(Schema):
class LineStringSchema(BaseSchema):
"""
A GeoJSON LineString
"""
@ -42,7 +73,7 @@ class LineStringSchema(Schema):
title = "GeoJSON LineString"
class LineSchema(Schema):
class LineSchema(BaseSchema):
"""
A GeoJSON LineString with only two points
"""
@ -55,7 +86,7 @@ class LineSchema(Schema):
title = "GeoJSON LineString (only two points)"
class PointSchema(Schema):
class PointSchema(BaseSchema):
"""
A GeoJSON Point
"""
@ -77,7 +108,7 @@ GeometrySchema = Annotated[
]
class AnyGeometrySchema(Schema):
class AnyGeometrySchema(BaseSchema):
"""
A GeoJSON Geometry
"""

View file

@ -125,7 +125,7 @@ def get_view_as_api(request, path: str):
resolved = resolve_editor_path_api(request, path)
if not resolved:
raise API404(str(_('No matching editor view endpoint found.')))
raise API404(_('No matching editor view endpoint found.'))
if not getattr(resolved.func, 'api_hybrid', False):
raise API404(_('Matching editor view point does not provide an API.'))

View file

@ -1,10 +1,9 @@
from typing import Annotated, Literal, Optional, Union
from ninja import Schema
from pydantic import Field as APIField
from pydantic import PositiveInt
from c3nav.api.schema import AnyGeometrySchema, GeometrySchema, LineSchema
from c3nav.api.schema import AnyGeometrySchema, GeometrySchema, LineSchema, BaseSchema
from c3nav.api.utils import NonEmptyStr
GeometryStylesSchema = Annotated[
@ -47,7 +46,7 @@ EditorGeometriesCacheReferenceElem = Annotated[
]
class EditorGeometriesPropertiesSchema(Schema):
class EditorGeometriesPropertiesSchema(BaseSchema):
id: EditorID
type: NonEmptyStr
space: Union[
@ -63,20 +62,20 @@ class EditorGeometriesPropertiesSchema(Schema):
opacity: Optional[float] = None # todo: range
class EditorGeometriesGraphEdgePropertiesSchema(Schema):
class EditorGeometriesGraphEdgePropertiesSchema(BaseSchema):
id: EditorID
type: Literal["graphedge"]
from_node: EditorID
to_node: EditorID
class EditorGeometriesGraphEdgeElemSchema(Schema):
class EditorGeometriesGraphEdgeElemSchema(BaseSchema):
type: Literal["Feature"]
properties: EditorGeometriesGraphEdgePropertiesSchema
geometry: LineSchema
class EditorGeometriesGeometryElemSchema(Schema):
class EditorGeometriesGeometryElemSchema(BaseSchema):
type: Literal["Feature"]
geometry: AnyGeometrySchema = APIField(description="geometry, potentially modified for displaying")
original_geometry: Optional[GeometrySchema] = APIField(

View file

@ -187,7 +187,7 @@ class APIHybridFormTemplateResponse(APIHybridResponse):
def get_api_response(self, request):
result = {}
if self.error:
result['error'] = str(self.error.message)
result['error'] = self.error.message
self.status_code = self.error.status_code
if request.method == 'POST':
if not self.form.is_valid():

View file

@ -6,12 +6,12 @@ from django.shortcuts import redirect
from django.utils import timezone
from ninja import Query
from ninja import Router as APIRouter
from ninja import Schema
from pydantic import Field as APIField
from pydantic import PositiveInt
from c3nav.api.auth import auth_permission_responses, auth_responses, validate_responses
from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidationFailed
from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.api.base import api_etag, api_stats, can_access_geometry
from c3nav.mapdata.models import Source
@ -39,7 +39,7 @@ def bounds(request):
}
class LocationEndpointParameters(Schema):
class LocationEndpointParameters(BaseSchema):
searchable: bool = APIField(
False,
title='only list searchable locations',
@ -133,7 +133,7 @@ def _location_geometry(request, location):
)
class ShowRedirects(Schema):
class ShowRedirects(BaseSchema):
show_redirects: bool = APIField(
False,
name="show redirects",
@ -268,7 +268,7 @@ def get_position_by_id(request, position_id: AnyPositionID):
return location.serialize_position()
class UpdatePositionSchema(Schema):
class UpdatePositionSchema(BaseSchema):
coordinates_id: Union[
Annotated[CustomLocationID, APIField(title="set coordinates")],
Annotated[None, APIField(title="unset coordinates")],

View file

@ -4,10 +4,10 @@ from urllib.parse import urlparse
from django.http import HttpResponse
from ninja import Field as APIField
from ninja import Router as APIRouter
from ninja import Schema
from pydantic import PositiveInt
from c3nav.api.auth import auth_responses
from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.models import MapUpdate
from c3nav.mapdata.utils.cache.stats import increment_cache_key
@ -17,7 +17,7 @@ from c3nav.mapdata.views import set_tile_access_cookie
updates_api_router = APIRouter(tags=["updates"])
class UserDataSchema(Schema):
class UserDataSchema(BaseSchema):
logged_in: bool = APIField(
title="logged in",
description="whether a user is logged in",
@ -52,7 +52,7 @@ class UserDataSchema(Schema):
)
class FetchUpdatesResponseSchema(Schema):
class FetchUpdatesResponseSchema(BaseSchema):
last_site_update: Optional[PositiveInt] = APIField(
title="ID of the last site update",
description="If this ID increments, it means a major code change may have occurred. "

View file

@ -74,7 +74,7 @@ class TitledMixin(SerializableMixin, models.Model):
result = super()._serialize(detailed=detailed, **kwargs)
if detailed:
result['titles'] = self.titles
result['title'] = str(self.title)
result['title'] = self.title
return result
@property

View file

@ -88,7 +88,7 @@ class LocationSlug(SerializableMixin, models.Model):
def details_display(self, **kwargs):
result = super().details_display(**kwargs)
result['display'].insert(2, (_('Slug'), str(self.get_slug())))
result['display'].insert(2, (_('Slug'), self.get_slug()))
return result
@cached_property
@ -120,7 +120,7 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
def _serialize(self, search=False, **kwargs):
result = super()._serialize(**kwargs)
result['subtitle'] = str(self.subtitle)
result['subtitle'] = self.subtitle
result['icon'] = self.get_icon()
result['can_search'] = self.can_search
result['can_describe'] = self.can_search
@ -202,7 +202,7 @@ class SpecificLocation(Location, models.Model):
result['label_settings'] = label_settings.serialize(detailed=False)
if self.label_overrides:
# todo: what if only one language is set?
result['label_override'] = str(self.label_override)
result['label_override'] = self.label_override
return result
def get_label_settings(self):
@ -300,9 +300,9 @@ class LocationGroupCategory(SerializableMixin, models.Model):
result['titles'] = self.titles
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['title'] = self.title
result['title_plural'] = self.title_plural
result['help_text'] = self.help_text
result['allow_levels'] = self.allow_levels
result['allow_spaces'] = self.allow_spaces
result['allow_areas'] = self.allow_areas

View file

@ -2,10 +2,10 @@ from typing import Literal, Optional, Type
from django.core.cache import cache
from django.db.models import Model, QuerySet
from ninja import Schema
from pydantic import Field as APIField
from c3nav.api.exceptions import APIRequestValidationFailed
from c3nav.api.schema import BaseSchema
from c3nav.mapdata.models import Level, LocationGroup, LocationGroupCategory, MapUpdate, Space
from c3nav.mapdata.models.access import AccessPermission
@ -38,7 +38,7 @@ def assert_valid_value(request, model: Type[Model], key: str, values: set):
raise APIRequestValidationFailed("Unknown %s: %r" % (model.__name__, remainder))
class FilterSchema(Schema):
class FilterSchema(BaseSchema):
def filter_qs(self, qs: QuerySet) -> QuerySet:
return qs

View file

@ -1,13 +1,11 @@
import re
from typing import Annotated, Any, Optional, Union
from typing import Annotated, Optional, Union
from ninja import Schema
from pydantic import Field as APIField
from pydantic import PositiveInt, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from pydantic_core.core_schema import ValidationInfo
from pydantic import PositiveInt
from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema
from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema, BaseSchema
from c3nav.api.utils import NonEmptyStr
@ -36,24 +34,14 @@ BoundsSchema = tuple[
]
class SerializableSchema(Schema):
@model_validator(mode="wrap") # noqa
@classmethod
def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any:
""" overwriting this, we need to call serialize to get the correct data """
if hasattr(values, 'serialize') and callable(values.serialize):
values = values.serialize()
return handler(values)
class DjangoModelSchema(SerializableSchema):
class DjangoModelSchema(BaseSchema):
id: PositiveInt = APIField(
title="ID",
example=1,
)
class LocationSlugSchema(Schema):
class LocationSlugSchema(BaseSchema):
slug: NonEmptyStr = APIField(
title="location slug",
description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs"
@ -65,7 +53,7 @@ class LocationSlugSchema(Schema):
)
class WithAccessRestrictionSchema(Schema):
class WithAccessRestrictionSchema(BaseSchema):
access_restriction: Union[
Annotated[PositiveInt, APIField(title="access restriction ID")],
Annotated[None, APIField(title="null", description="no access restriction")],
@ -76,7 +64,7 @@ class WithAccessRestrictionSchema(Schema):
)
class TitledSchema(Schema):
class TitledSchema(BaseSchema):
titles: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="title (all languages)",
description="title in all available languages. property names are the ISO-language code. "
@ -213,7 +201,7 @@ class SpecificLocationSchema(LocationSchema):
)
class WithPolygonGeometrySchema(Schema):
class WithPolygonGeometrySchema(BaseSchema):
geometry: Union[
PolygonSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
@ -224,7 +212,7 @@ class WithPolygonGeometrySchema(Schema):
)
class WithLineStringGeometrySchema(Schema):
class WithLineStringGeometrySchema(BaseSchema):
geometry: Union[
LineStringSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
@ -235,7 +223,7 @@ class WithLineStringGeometrySchema(Schema):
)
class WithPointGeometrySchema(Schema):
class WithPointGeometrySchema(BaseSchema):
geometry: Union[
PointSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
@ -246,7 +234,7 @@ class WithPointGeometrySchema(Schema):
)
class WithLevelSchema(SerializableSchema):
class WithLevelSchema(BaseSchema):
level: PositiveInt = APIField(
title="level",
description="level id this object belongs to.",
@ -254,7 +242,7 @@ class WithLevelSchema(SerializableSchema):
)
class WithSpaceSchema(SerializableSchema):
class WithSpaceSchema(BaseSchema):
space: PositiveInt = APIField(
title="space",
description="space id this object belongs to.",
@ -262,7 +250,7 @@ class WithSpaceSchema(SerializableSchema):
)
class SimpleGeometryPointSchema(Schema):
class SimpleGeometryPointSchema(BaseSchema):
point: tuple[
Annotated[PositiveInt, APIField(title="level ID")],
Annotated[float, APIField(title="x coordinate")],
@ -281,7 +269,7 @@ class SimpleGeometryPointAndBoundsSchema(SimpleGeometryPointSchema):
)
class SimpleGeometryLocationsSchema(Schema):
class SimpleGeometryLocationsSchema(BaseSchema):
locations: list[PositiveInt] = APIField( # todo: this should be a set… but json serialization?
description="IDs of all locations that belong to this grouo",
example=(1, 2, 3),

View file

@ -5,10 +5,10 @@ from pydantic import Discriminator
from pydantic import Field as APIField
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
from c3nav.api.schema import GeometrySchema, PointSchema
from c3nav.api.schema import GeometrySchema, PointSchema, BaseSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, CustomLocationID, DjangoModelSchema,
LabelSettingsSchema, LocationSchema, PositionID, SerializableSchema,
LabelSettingsSchema, LocationSchema, PositionID,
SimpleGeometryLocationsSchema, SimpleGeometryPointAndBoundsSchema,
SimpleGeometryPointSchema, SpecificLocationSchema, TitledSchema,
WithAccessRestrictionSchema, WithLevelSchema,
@ -357,7 +357,7 @@ class AccessRestrictionGroupSchema(WithAccessRestrictionSchema, DjangoModelSchem
pass
class CustomLocationSchema(SerializableSchema):
class CustomLocationSchema(BaseSchema):
"""
A custom location represents coordinates that have been put in or calculated.
@ -439,7 +439,7 @@ class CustomLocationSchema(SerializableSchema):
)
class TrackablePositionSchema(Schema):
class TrackablePositionSchema(BaseSchema):
"""
A trackable position. Its position can be set or reset.
"""
@ -468,7 +468,7 @@ class TrackablePositionSchema(Schema):
)
class LocationTypeSchema(Schema):
class LocationTypeSchema(BaseSchema):
locationtype: str = APIField(title="location type",
description="indicates what kind of location is included. "
"different location types have different fields.")
@ -544,7 +544,7 @@ class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchem
locationtype: Literal["position"] = LocationTypeAPIField()
class SlimLocationMixin(Schema):
class SlimLocationMixin(BaseSchema):
level: ClassVar[None]
space: ClassVar[None]
titles: ClassVar[None]
@ -662,7 +662,7 @@ all_location_definitions = listable_location_definitions + "\n" + schema_definit
)
class DisplayLink(Schema):
class DisplayLink(BaseSchema):
"""
A link for the location display
"""
@ -672,7 +672,7 @@ class DisplayLink(Schema):
can_search: bool
class LocationDisplay(SerializableSchema):
class LocationDisplay(BaseSchema):
id: AnyLocationID = APIField(
description="a numeric ID for a map location or a string ID for generated locations",
example=1,
@ -749,7 +749,7 @@ class LocationDisplay(SerializableSchema):
)
class PositionStatusSchema(Schema):
class PositionStatusSchema(BaseSchema):
id: AnyPositionID = APIField(
description="the ID of the dynamic position that has been queries",
)
@ -758,7 +758,7 @@ class PositionStatusSchema(Schema):
)
class PositionAvailabilitySchema(Schema):
class PositionAvailabilitySchema(BaseSchema):
available: str

View file

@ -1,14 +1,13 @@
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.api.schema import GeometrySchema, BaseSchema
from c3nav.mapdata.schemas.model_base import AnyLocationID, BoundsSchema
class WithBoundsSchema(Schema):
class WithBoundsSchema(BaseSchema):
"""
Describing a bounding box
"""
@ -18,7 +17,7 @@ class WithBoundsSchema(Schema):
)
class LocationGeometry(Schema):
class LocationGeometry(BaseSchema):
id: AnyLocationID = APIField(
description="ID of the location that the geometry is being queried for",
)

View file

@ -416,7 +416,7 @@ class CustomLocation:
@cached_property
def grid_square(self):
return grid.get_square_for_point(self.x, self.y) or ''
return grid.get_square_for_point(self.x, self.y)
@cached_property
def title_subtitle(self):

View file

@ -17,13 +17,13 @@ def get_user_data(request):
}
if permissions:
result.update({
'title': str(_('not logged in')),
'title': _('not logged in'),
'subtitle': ngettext_lazy('%d area unlocked', '%d areas unlocked', len(permissions)) % len(permissions),
'permissions': tuple(permissions),
})
else:
result.update({
'title': str(_('Login')),
'title': _('Login'),
'subtitle': None,
'permissions': (),
})

View file

@ -11,6 +11,7 @@ from pydantic import PositiveInt, field_validator
from c3nav.api.auth import APIKeyAuth, auth_permission_responses, auth_responses, validate_responses
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
from c3nav.api.schema import BaseSchema
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
from c3nav.mesh.messages import MeshMessageType
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage
@ -18,7 +19,7 @@ from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage
mesh_api_router = APIRouter(tags=["mesh"], auth=APIKeyAuth(permissions={"mesh_control"}))
class FirmwareBuildSchema(Schema):
class FirmwareBuildSchema(BaseSchema):
"""
A build belonging to a firmware version.
"""
@ -53,7 +54,7 @@ class FirmwareBuildSchema(Schema):
pass
class FirmwareSchema(Schema):
class FirmwareSchema(BaseSchema):
"""
A firmware version, usually with multiple build variants.
"""
@ -128,7 +129,7 @@ def firmware_project_description(request, firmware_id: int, variant: str):
raise API404("Firmware or firmware build not found")
class UploadFirmwareBuildSchema(Schema):
class UploadFirmwareBuildSchema(BaseSchema):
"""
A firmware build to upload, with at least one build variant
"""
@ -138,7 +139,7 @@ class UploadFirmwareBuildSchema(Schema):
uploaded_filename: str = APIField(..., example="firmware.bin")
class UploadFirmwareSchema(Schema):
class UploadFirmwareSchema(BaseSchema):
"""
A firmware version to upload, with at least one build variant
"""
@ -205,14 +206,14 @@ def firmware_upload(request, firmware_data: UploadFirmwareSchema, binary_files:
NodeAddress = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$")]
class MessagesFilter(Schema):
class MessagesFilter(BaseSchema):
src_node: Optional[NodeAddress] = None
msg_type: Optional[MeshMessageType] = None
time_from: Optional[datetime] = None
time_until: Optional[datetime] = None
class NodeMessageSchema(Schema):
class NodeMessageSchema(BaseSchema):
id: int
src_node: NodeAddress
message_type: MeshMessageType

View file

@ -3,9 +3,9 @@ from typing import Annotated, Union
from django.core.exceptions import ValidationError
from ninja import Field as APIField
from ninja import Router as APIRouter
from ninja import Schema
from c3nav.api.auth import auth_responses
from c3nav.api.schema import BaseSchema
from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.schemas.models import CustomLocationSchema
from c3nav.mapdata.utils.cache.stats import increment_cache_key
@ -15,13 +15,13 @@ from c3nav.routing.schemas import BSSIDSchema, LocateRequestPeerSchema
positioning_api_router = APIRouter(tags=["positioning"])
class LocateRequestSchema(Schema):
class LocateRequestSchema(BaseSchema):
peers: list[LocateRequestPeerSchema] = APIField(
title="list of visible/measured location beacons",
)
class PositioningResult(Schema):
class PositioningResult(BaseSchema):
location: Union[
Annotated[CustomLocationSchema, APIField(title="location")],
Annotated[None, APIField(title="null", description="position could not be determined")]

View file

@ -11,6 +11,7 @@ from pydantic import PositiveInt
from c3nav.api.auth import APIKeyAuth, auth_responses, validate_responses
from c3nav.api.exceptions import APIRequestValidationFailed
from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.api.base import api_stats_clean_location_value
from c3nav.mapdata.models.access import AccessPermission
@ -53,7 +54,7 @@ class AltitudeWayTypeChoice(StrEnum):
AVOID = "avoid"
class UpdateRouteOptionsSchema(Schema):
class UpdateRouteOptionsSchema(BaseSchema):
# todo: default is wrong, this should be optional
mode: Union[
Annotated[RouteMode, APIField(title="route mode", description="routing mode to use")],
@ -81,7 +82,7 @@ class UpdateRouteOptionsSchema(Schema):
)
class RouteOptionsSchema(Schema):
class RouteOptionsSchema(BaseSchema):
# todo: default is wrong, this should be optional
mode: RouteMode = APIField(name="routing mode")
walk_speed: WalkSpeed = APIField(name="walk speed")
@ -96,7 +97,7 @@ class RouteOptionsSchema(Schema):
)
class RouteParametersSchema(Schema):
class RouteParametersSchema(BaseSchema):
origin: AnyLocationID
destination: AnyLocationID
options_override: Optional[UpdateRouteOptionsSchema] = APIField(
@ -105,7 +106,7 @@ class RouteParametersSchema(Schema):
)
class RouteItemSchema(Schema):
class RouteItemSchema(BaseSchema):
id: PositiveInt
coordinates: Coordinates3D
waytype: Union[
@ -132,7 +133,7 @@ class RouteItemSchema(Schema):
]]
class RouteSchema(Schema):
class RouteSchema(BaseSchema):
origin: dict # todo: improve this
destination: dict # todo: improve this
distance: float
@ -144,7 +145,7 @@ class RouteSchema(Schema):
items: list[RouteItemSchema]
class RouteResponse(Schema):
class RouteResponse(BaseSchema):
request: RouteParametersSchema
options: RouteOptionsSchema
report_issue_url: NonEmptyStr
@ -154,7 +155,7 @@ class RouteResponse(Schema):
title = "route found"
class NoRouteResponse(Schema):
class NoRouteResponse(BaseSchema):
request: RouteParametersSchema
options: RouteOptionsSchema
error: NonEmptyStr = APIField(
@ -197,19 +198,19 @@ def get_route(request, parameters: RouteParametersSchema):
return NoRouteResponse(
request=parameters,
options=_new_serialize_route_options(options),
error=str(_('Not yet routable, try again shortly.')),
error=_('Not yet routable, try again shortly.'),
)
except LocationUnreachable:
return NoRouteResponse(
request=parameters,
options=_new_serialize_route_options(options),
error=str(_('Unreachable location.'))
error=_('Unreachable location.')
)
except NoRouteFound:
return NoRouteResponse(
request=parameters,
options=_new_serialize_route_options(options),
error=str(_('No route found.'))
error=_('No route found.')
)
origin_values = api_stats_clean_location_value(form.cleaned_data['origin'].pk)
@ -282,12 +283,12 @@ def set_route_options(request, new_options: UpdateRouteOptionsSchema):
return _new_serialize_route_options(options)
class RouteOptionsFieldChoices(Schema):
class RouteOptionsFieldChoices(BaseSchema):
name: NonEmptyStr
title: NonEmptyStr
class RouteOptionsField(Schema):
class RouteOptionsField(BaseSchema):
name: NonEmptyStr
type: NonEmptyStr
label: NonEmptyStr

View file

@ -240,9 +240,7 @@ class RouteItem:
if self.new_level:
result['level'] = describe_location(self.level, locations)
# convert all the string proxies to strings
# todo: better to let the api response serializer do this somehow?
result['descriptions'] = [(icon, str(instruction)) for (icon, instruction) in self.descriptions]
result['descriptions'] = [(icon, instruction) for (icon, instruction) in self.descriptions]
return result

View file

@ -1,15 +1,15 @@
from typing import Annotated, Union
from ninja import Schema
from pydantic import Field as APIField
from pydantic import NegativeInt, PositiveInt
from c3nav.api.schema import BaseSchema
from c3nav.api.utils import NonEmptyStr
BSSIDSchema = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$", title="BSSID")]
class LocateRequestPeerSchema(Schema):
class LocateRequestPeerSchema(BaseSchema):
bssid: BSSIDSchema = APIField(
title="BSSID",
description="BSSID of the peer",