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:
parent
4c06abd400
commit
4b1ac9f194
21 changed files with 126 additions and 104 deletions
|
@ -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."
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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.'))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")],
|
||||
|
|
|
@ -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. "
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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': (),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue