new routing api, now available
This commit is contained in:
parent
55f5dfb0b7
commit
f7f77b2823
2 changed files with 135 additions and 15 deletions
|
@ -164,16 +164,16 @@ class RouteOptions(models.Model):
|
||||||
{
|
{
|
||||||
'name': name,
|
'name': name,
|
||||||
'type': field.widget.input_type,
|
'type': field.widget.input_type,
|
||||||
'label': field.label,
|
'label': str(field.label),
|
||||||
'choices': [
|
'choices': [
|
||||||
{
|
{
|
||||||
'name': choice_name,
|
'name': choice_name,
|
||||||
'title': choice_title,
|
'title': str(choice_title),
|
||||||
}
|
}
|
||||||
for choice_name, choice_title in field.choices
|
for choice_name, choice_title in field.choices
|
||||||
],
|
],
|
||||||
'value': self[name],
|
'value': self[name],
|
||||||
'value_display': dict(field.choices)[self[name]],
|
'value_display': str(dict(field.choices)[self[name]]),
|
||||||
}
|
}
|
||||||
for name, field in self.get_fields().items()
|
for name, field in self.get_fields().items()
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Annotated, Optional, Union
|
from typing import Annotated, Optional, Union
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from ninja import Field as APIField
|
from ninja import Field as APIField
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import PositiveInt
|
from pydantic import PositiveInt
|
||||||
|
|
||||||
|
from c3nav.api.exceptions import APIRequestValidationFailed
|
||||||
from c3nav.api.newauth import auth_responses, validate_responses
|
from c3nav.api.newauth import auth_responses, validate_responses
|
||||||
from c3nav.api.utils import NonEmptyStr
|
from c3nav.api.utils import NonEmptyStr
|
||||||
from c3nav.mapdata.models import Source
|
from c3nav.mapdata.api import api_stats_clean_location_value
|
||||||
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
|
from c3nav.mapdata.models.locations import Position
|
||||||
from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D
|
from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D
|
||||||
from c3nav.mapdata.schemas.responses import BoundsSchema
|
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||||
|
from c3nav.mapdata.utils.locations import visible_locations_for_request
|
||||||
|
from c3nav.routing.exceptions import NotYetRoutable, LocationUnreachable, NoRouteFound
|
||||||
|
from c3nav.routing.forms import RouteForm
|
||||||
|
from c3nav.routing.models import RouteOptions
|
||||||
|
from c3nav.routing.router import Router
|
||||||
|
|
||||||
routing_api_router = APIRouter(tags=["routing"])
|
routing_api_router = APIRouter(tags=["routing"])
|
||||||
|
|
||||||
|
@ -81,26 +92,130 @@ class RouteSchema(Schema):
|
||||||
items: list[RouteItemSchema]
|
items: list[RouteItemSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class RouteResponse(Schema):
|
||||||
|
request: RouteParametersSchema
|
||||||
|
options: RouteOptionsSchema
|
||||||
|
report_issue_url: NonEmptyStr
|
||||||
|
result: RouteSchema
|
||||||
|
|
||||||
|
|
||||||
|
class NoRouteResponse(Schema):
|
||||||
|
""" the routing parameters were valid, but it was not possible to determine a route for these parameters """
|
||||||
|
request: RouteParametersSchema
|
||||||
|
options: RouteOptionsSchema
|
||||||
|
error: NonEmptyStr = APIField(name="error description")
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_pk(location):
|
||||||
|
return location.slug if isinstance(location, Position) else location.pk
|
||||||
|
|
||||||
|
|
||||||
@routing_api_router.post('/route/', summary="get route between two locations",
|
@routing_api_router.post('/route/', summary="get route between two locations",
|
||||||
response={200: RouteSchema, **validate_responses, **auth_responses})
|
response={200: RouteResponse | NoRouteResponse, **validate_responses, **auth_responses})
|
||||||
# todo: route failure responses
|
# todo: route failure responses
|
||||||
def get_route(request, parameters: RouteParametersSchema):
|
def get_route(request, parameters: RouteParametersSchema):
|
||||||
# todo: implement
|
form = RouteForm({
|
||||||
raise NotImplementedError
|
"origin": parameters.origin,
|
||||||
|
"destination": parameters.destination,
|
||||||
|
}, request=request)
|
||||||
|
|
||||||
|
if not form.is_valid():
|
||||||
|
return APIRequestValidationFailed("\n".join(form.errors))
|
||||||
|
|
||||||
|
options = RouteOptions.get_for_request(request)
|
||||||
|
_new_update_route_options(options, parameters.options_override)
|
||||||
|
|
||||||
|
try:
|
||||||
|
route = Router.load().get_route(origin=form.cleaned_data['origin'],
|
||||||
|
destination=form.cleaned_data['destination'],
|
||||||
|
permissions=AccessPermission.get_for_request(request),
|
||||||
|
options=options)
|
||||||
|
except NotYetRoutable:
|
||||||
|
return NoRouteResponse(
|
||||||
|
request=parameters,
|
||||||
|
options=_new_serialize_route_options(options),
|
||||||
|
error=str(_('Not yet routable, try again shortly.')),
|
||||||
|
)
|
||||||
|
except LocationUnreachable:
|
||||||
|
return NoRouteResponse(
|
||||||
|
request=parameters,
|
||||||
|
options=_new_serialize_route_options(options),
|
||||||
|
error=str(_('Unreachable location.'))
|
||||||
|
)
|
||||||
|
except NoRouteFound:
|
||||||
|
return NoRouteResponse(
|
||||||
|
request=parameters,
|
||||||
|
options=_new_serialize_route_options(options),
|
||||||
|
error=str(_('No route found.'))
|
||||||
|
)
|
||||||
|
|
||||||
|
origin_values = api_stats_clean_location_value(form.cleaned_data['origin'].pk)
|
||||||
|
destination_values = api_stats_clean_location_value(form.cleaned_data['destination'].pk)
|
||||||
|
increment_cache_key('apistats__route')
|
||||||
|
for origin_value in origin_values:
|
||||||
|
for destination_value in destination_values:
|
||||||
|
increment_cache_key('apistats__route_tuple_%s_%s' % (origin_value, destination_value))
|
||||||
|
for value in origin_values:
|
||||||
|
increment_cache_key('apistats__route_origin_%s' % value)
|
||||||
|
for value in destination_values:
|
||||||
|
increment_cache_key('apistats__route_destination_%s' % value)
|
||||||
|
|
||||||
|
return RouteResponse(
|
||||||
|
request=parameters,
|
||||||
|
options=_new_serialize_route_options(options),
|
||||||
|
report_issue_url=reverse('site.report_create', kwargs={
|
||||||
|
'origin': request.POST['origin'],
|
||||||
|
'destination': request.POST['destination'],
|
||||||
|
'options': options.serialize_string()
|
||||||
|
}),
|
||||||
|
result=route.serialize(locations=visible_locations_for_request(request)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _new_serialize_route_options(options):
|
||||||
|
# todo: RouteOptions should obviously be modernized
|
||||||
|
main_options = {}
|
||||||
|
waytype_options = {}
|
||||||
|
for key, value in options.items():
|
||||||
|
if key.startswith("waytype_"):
|
||||||
|
waytype_options[key.removeprefix("waytype_")] = value
|
||||||
|
else:
|
||||||
|
main_options[key] = value
|
||||||
|
return {
|
||||||
|
**main_options,
|
||||||
|
"way_types": waytype_options,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _new_update_route_options(options, new_options):
|
||||||
|
convert_options = new_options.dict()
|
||||||
|
waytype_options = convert_options.pop("way_types", {})
|
||||||
|
convert_options.update({f"waytype_{key}": value for key, value in waytype_options.items()})
|
||||||
|
|
||||||
|
try:
|
||||||
|
options.update(waytype_options, ignore_unknown=True)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise APIRequestValidationFailed(str(e))
|
||||||
|
|
||||||
|
|
||||||
@routing_api_router.get('/options/', summary="get current route options",
|
@routing_api_router.get('/options/', summary="get current route options",
|
||||||
response={200: RouteOptionsSchema, **auth_responses})
|
response={200: RouteOptionsSchema, **auth_responses})
|
||||||
def get_route_options(request):
|
def get_route_options(request):
|
||||||
# todo: implement
|
options = RouteOptions.get_for_request(request)
|
||||||
raise NotImplementedError
|
return _new_serialize_route_options(options)
|
||||||
|
|
||||||
|
|
||||||
@routing_api_router.put('/options/', summary="set route options for user or session",
|
@routing_api_router.put('/options/', summary="set route options for user or session",
|
||||||
response={200: RouteOptionsSchema, **validate_responses, **auth_responses})
|
response={200: RouteOptionsSchema, **validate_responses, **auth_responses})
|
||||||
def set_route_options(request, options: RouteOptionsSchema):
|
def set_route_options(request, new_options: RouteOptionsSchema):
|
||||||
# todo: implement
|
options = RouteOptions.get_for_request(request)
|
||||||
raise NotImplementedError
|
|
||||||
|
_new_update_route_options(options, new_options)
|
||||||
|
options.save()
|
||||||
|
|
||||||
|
return _new_serialize_route_options(options)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RouteOptionsFieldChoices(Schema):
|
class RouteOptionsFieldChoices(Schema):
|
||||||
|
@ -120,5 +235,10 @@ class RouteOptionsField(Schema):
|
||||||
@routing_api_router.get('/options/form/', summary="get current route options with form definitions (like old API)",
|
@routing_api_router.get('/options/form/', summary="get current route options with form definitions (like old API)",
|
||||||
response={200: list[RouteOptionsField], **auth_responses})
|
response={200: list[RouteOptionsField], **auth_responses})
|
||||||
def get_route_options_form(request):
|
def get_route_options_form(request):
|
||||||
# todo: implement
|
options = RouteOptions.get_for_request(request)
|
||||||
raise NotImplementedError
|
data = options.serialize()
|
||||||
|
for option in data:
|
||||||
|
if option["name"].startswith("waytype_"):
|
||||||
|
option["name"] = "way_types."+data["name"].removeprefix("waytype_")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue