2024-12-02 23:40:41 +01:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from typing import Optional, Sequence, Type, Callable, Any
|
2023-11-19 15:34:08 +01:00
|
|
|
|
2024-12-26 21:49:07 +01:00
|
|
|
from django.db import transaction
|
|
|
|
from django.db.models import Model, F
|
|
|
|
from django.shortcuts import get_object_or_404
|
2023-11-19 15:34:08 +01:00
|
|
|
from ninja import Query
|
|
|
|
from ninja import Router as APIRouter
|
2024-12-02 23:40:41 +01:00
|
|
|
from pydantic import PositiveInt
|
2023-11-19 15:34:08 +01:00
|
|
|
|
2024-12-26 21:49:07 +01:00
|
|
|
from c3nav.api.auth import auth_responses, validate_responses, auth_permission_responses
|
|
|
|
from c3nav.api.exceptions import API404, APIPermissionDenied
|
2024-12-02 23:40:41 +01:00
|
|
|
from c3nav.api.schema import BaseSchema
|
2024-12-03 17:07:07 +01:00
|
|
|
from c3nav.mapdata.api.base import api_etag, optimize_query, can_access_geometry
|
2023-11-19 16:36:46 +01:00
|
|
|
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
|
2024-12-16 20:42:28 +01:00
|
|
|
Space, Stair, DataOverlay, DataOverlayFeature, WayType)
|
2024-12-26 21:49:07 +01:00
|
|
|
from c3nav.mapdata.models.access import AccessRestriction, AccessRestrictionGroup, AccessPermission
|
2023-11-19 16:36:46 +01:00
|
|
|
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
|
|
|
Obstacle, Ramp)
|
2024-12-04 12:08:19 +01:00
|
|
|
from c3nav.mapdata.models.locations import DynamicLocation, LabelSettings, LocationRedirect
|
2023-11-23 18:41:46 +01:00
|
|
|
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema,
|
2024-12-03 09:59:51 +01:00
|
|
|
LevelGeometryFilter, SpaceGeometryFilter, BySpaceFilter, ByOverlayFilter)
|
2024-12-04 11:58:20 +01:00
|
|
|
from c3nav.mapdata.schemas.model_base import schema_description, LabelSettingsSchema
|
2023-11-19 16:48:40 +01:00
|
|
|
from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema,
|
2023-11-23 23:22:30 +01:00
|
|
|
BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema,
|
|
|
|
DynamicLocationSchema, HoleSchema, LeaveDescriptionSchema, LevelSchema,
|
|
|
|
LineObstacleSchema, LocationGroupCategorySchema, LocationGroupSchema,
|
2024-11-21 11:56:31 +01:00
|
|
|
ObstacleSchema, POISchema, RampSchema, SourceSchema, SpaceSchema, StairSchema,
|
2024-12-16 20:42:28 +01:00
|
|
|
DataOverlaySchema, DataOverlayFeatureSchema, LocationRedirectSchema,
|
2024-12-26 21:50:21 +01:00
|
|
|
WayTypeSchema, DataOverlayFeatureGeometrySchema,
|
|
|
|
DataOverlayFeatureUpdateSchema, DataOverlayFeatureBulkUpdateSchema,
|
|
|
|
)
|
2023-11-19 15:34:08 +01:00
|
|
|
|
|
|
|
mapdata_api_router = APIRouter(tags=["mapdata"])
|
|
|
|
|
|
|
|
|
|
|
|
def mapdata_list_endpoint(request,
|
|
|
|
model: Type[Model],
|
|
|
|
filters: Optional[FilterSchema] = None,
|
|
|
|
order_by: Sequence[str] = ('pk',)):
|
|
|
|
# validate filters
|
|
|
|
if filters:
|
|
|
|
filters.validate(request)
|
|
|
|
|
|
|
|
# get the queryset and filter it
|
|
|
|
qs = optimize_query(
|
|
|
|
model.qs_for_request(request) if hasattr(model, 'qs_for_request') else model.objects.all()
|
|
|
|
)
|
|
|
|
if filters:
|
2024-12-03 17:02:02 +01:00
|
|
|
qs = filters.filter_qs(request, qs)
|
2023-11-19 15:34:08 +01:00
|
|
|
|
|
|
|
# order_by
|
|
|
|
qs = qs.order_by(*order_by)
|
|
|
|
|
2024-12-03 18:42:33 +01:00
|
|
|
result = list(qs)
|
2023-11-19 15:34:08 +01:00
|
|
|
|
2024-12-03 18:42:33 +01:00
|
|
|
for obj in result:
|
2024-12-16 10:57:50 +01:00
|
|
|
if not can_access_geometry(request, obj):
|
2024-12-03 18:42:33 +01:00
|
|
|
obj._hide_geometry = True
|
|
|
|
|
|
|
|
return result
|
2023-11-19 15:34:08 +01:00
|
|
|
|
|
|
|
|
|
|
|
def mapdata_retrieve_endpoint(request, model: Type[Model], **lookups):
|
|
|
|
try:
|
2024-12-03 17:07:07 +01:00
|
|
|
obj = optimize_query(
|
2023-11-19 15:34:08 +01:00
|
|
|
model.qs_for_request(request) if hasattr(model, 'qs_for_request') else model.objects.all()
|
|
|
|
).get(**lookups)
|
2024-12-03 17:07:07 +01:00
|
|
|
if not can_access_geometry(request, obj):
|
|
|
|
obj.geometry = None
|
|
|
|
return obj
|
2023-11-19 15:34:08 +01:00
|
|
|
except model.DoesNotExist:
|
|
|
|
raise API404("%s not found" % model.__name__.lower())
|
|
|
|
|
|
|
|
|
2024-12-02 23:40:41 +01:00
|
|
|
@dataclass
|
|
|
|
class MapdataEndpoint:
|
|
|
|
model: Type[Model]
|
|
|
|
schema: Type[BaseSchema]
|
|
|
|
filters: Type[FilterSchema] | None = None
|
2024-12-26 21:50:21 +01:00
|
|
|
no_cache: bool = False
|
|
|
|
name: Optional[str] = None
|
2024-12-02 23:40:41 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def model_name(self):
|
|
|
|
return self.model._meta.model_name
|
|
|
|
|
|
|
|
@property
|
2024-12-26 21:50:21 +01:00
|
|
|
def endpoint_name(self):
|
|
|
|
return self.name if self.name is not None else self.model._meta.default_related_name
|
2024-12-02 23:40:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class MapdataAPIBuilder:
|
|
|
|
router: APIRouter
|
|
|
|
|
|
|
|
def build_all_endpoints(self, endpoints: dict[str, list[MapdataEndpoint]]):
|
|
|
|
for tag, endpoints in endpoints.items():
|
|
|
|
for endpoint in endpoints:
|
|
|
|
self.add_endpoints(endpoint, tag=tag)
|
|
|
|
|
|
|
|
def add_endpoints(self, endpoint: MapdataEndpoint, tag: str):
|
|
|
|
self.add_list_endpoint(endpoint, tag=tag)
|
|
|
|
self.add_by_id_endpoint(endpoint, tag=tag)
|
|
|
|
|
|
|
|
def common_params(self, endpoint: MapdataEndpoint) -> dict[str: Any]:
|
|
|
|
return {"request": None}
|
|
|
|
|
|
|
|
def _make_endpoint(self, view_params: dict[str, str], call_func: Callable,
|
|
|
|
add_call_params: dict[str, str] = None) -> Callable:
|
|
|
|
|
|
|
|
if add_call_params is None:
|
|
|
|
add_call_params = {}
|
|
|
|
call_param_values = set(add_call_params.values())
|
|
|
|
call_params = (
|
2024-12-26 21:50:21 +01:00
|
|
|
*(f"{name}={name}" for name in set(view_params.keys()) - call_param_values),
|
2024-12-02 23:40:41 +01:00
|
|
|
*(f"{name}={value}" for name, value in add_call_params.items()),
|
|
|
|
)
|
|
|
|
method_code = "\n".join((
|
|
|
|
f"def gen_func({", ".join((f"{name}: {hint}" if hint else name) for name, hint in view_params.items())}):",
|
2024-12-03 11:13:13 +01:00
|
|
|
f" return call_func({", ".join(call_params)})",
|
2024-12-02 23:40:41 +01:00
|
|
|
))
|
2024-12-03 11:13:13 +01:00
|
|
|
g = {
|
|
|
|
**globals(),
|
|
|
|
"call_func": call_func,
|
|
|
|
}
|
|
|
|
exec(method_code, g)
|
|
|
|
return g["gen_func"] # noqa
|
2024-12-02 23:40:41 +01:00
|
|
|
|
|
|
|
def add_list_endpoint(self, endpoint: MapdataEndpoint, tag: str):
|
|
|
|
view_params = self.common_params(endpoint)
|
|
|
|
|
|
|
|
Query # noqa
|
|
|
|
if endpoint.filters:
|
|
|
|
filters_name = endpoint.filters.__name__
|
|
|
|
view_params["filters"] = f"Query[{filters_name}]"
|
|
|
|
|
|
|
|
list_func = self._make_endpoint(
|
|
|
|
view_params=view_params,
|
|
|
|
call_func=mapdata_list_endpoint,
|
|
|
|
add_call_params={"model": endpoint.model.__name__}
|
|
|
|
)
|
|
|
|
list_func.__name__ = f"{endpoint.model_name}_list"
|
|
|
|
|
2024-12-26 21:50:21 +01:00
|
|
|
if not endpoint.no_cache:
|
|
|
|
list_func = api_etag()(list_func)
|
|
|
|
|
|
|
|
self.router.get(f"/{endpoint.endpoint_name}/", summary=f"{endpoint.model_name} list",
|
2024-12-02 23:40:41 +01:00
|
|
|
tags=[f"mapdata-{tag}"], description=schema_description(endpoint.schema),
|
|
|
|
response={200: list[endpoint.schema],
|
|
|
|
**(validate_responses if endpoint.filters else {}),
|
|
|
|
**auth_responses})(
|
2024-12-26 21:50:21 +01:00
|
|
|
list_func
|
2024-12-02 23:40:41 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def add_by_id_endpoint(self, endpoint: MapdataEndpoint, tag: str):
|
|
|
|
view_params = self.common_params(endpoint)
|
|
|
|
PositiveInt # noqa
|
|
|
|
id_field = f"{endpoint.model_name}_id"
|
|
|
|
view_params[id_field] = "PositiveInt"
|
|
|
|
|
|
|
|
list_func = self._make_endpoint(
|
|
|
|
view_params=view_params,
|
|
|
|
call_func=mapdata_retrieve_endpoint,
|
|
|
|
add_call_params={"model": endpoint.model.__name__, "pk": id_field}
|
|
|
|
)
|
2024-12-03 09:59:51 +01:00
|
|
|
list_func.__name__ = f"{endpoint.model_name}_by_id"
|
2024-12-02 23:40:41 +01:00
|
|
|
|
2024-12-26 21:50:21 +01:00
|
|
|
self.router.get(f'/{endpoint.endpoint_name}/{{{id_field}}}/', summary=f"{endpoint.model_name} by ID",
|
2024-12-02 23:40:41 +01:00
|
|
|
tags=[f"mapdata-{tag}"], description=schema_description(endpoint.schema),
|
|
|
|
response={200: endpoint.schema, **API404.dict(), **auth_responses})(
|
|
|
|
api_etag()(list_func)
|
|
|
|
)
|
2023-11-19 15:34:08 +01:00
|
|
|
|
|
|
|
|
|
|
|
class LevelFilters(ByGroupFilter, ByOnTopOfFilter):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-11-23 18:41:46 +01:00
|
|
|
class SpaceFilters(ByGroupFilter, LevelGeometryFilter):
|
2023-11-19 15:34:08 +01:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-11-23 18:41:46 +01:00
|
|
|
class AreaFilters(ByGroupFilter, SpaceGeometryFilter):
|
2023-11-19 15:34:08 +01:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2024-12-02 23:40:41 +01:00
|
|
|
mapdata_endpoints: dict[str, list[MapdataEndpoint]] = {
|
|
|
|
"root": [
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Level,
|
|
|
|
schema=LevelSchema,
|
|
|
|
filters=LevelFilters
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=LocationGroup,
|
|
|
|
schema=LocationGroupSchema,
|
|
|
|
filters=ByCategoryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=LocationGroupCategory,
|
|
|
|
schema=LocationGroupCategorySchema,
|
|
|
|
),
|
2024-12-04 12:08:19 +01:00
|
|
|
MapdataEndpoint(
|
|
|
|
model=LocationRedirect,
|
|
|
|
schema=LocationRedirectSchema,
|
|
|
|
),
|
2024-12-02 23:40:41 +01:00
|
|
|
MapdataEndpoint(
|
|
|
|
model=Source,
|
|
|
|
schema=SourceSchema,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=AccessRestriction,
|
|
|
|
schema=AccessRestrictionSchema,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=AccessRestrictionGroup,
|
|
|
|
schema=AccessRestrictionGroupSchema,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=DynamicLocation,
|
|
|
|
schema=DynamicLocationSchema,
|
|
|
|
),
|
2024-12-04 11:58:20 +01:00
|
|
|
MapdataEndpoint(
|
|
|
|
model=LabelSettings,
|
|
|
|
schema=LabelSettingsSchema,
|
|
|
|
),
|
2024-12-03 09:59:51 +01:00
|
|
|
MapdataEndpoint(
|
|
|
|
model=DataOverlay,
|
|
|
|
schema=DataOverlaySchema,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=DataOverlayFeature,
|
|
|
|
schema=DataOverlayFeatureSchema,
|
|
|
|
filters=ByOverlayFilter,
|
2024-12-26 21:50:21 +01:00
|
|
|
no_cache=True,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=DataOverlayFeature,
|
|
|
|
schema=DataOverlayFeatureGeometrySchema,
|
|
|
|
filters=ByOverlayFilter,
|
|
|
|
no_cache=True,
|
|
|
|
name='dataoverlayfeaturegeometries'
|
2024-12-03 09:59:51 +01:00
|
|
|
),
|
2024-12-16 20:42:28 +01:00
|
|
|
MapdataEndpoint(
|
|
|
|
model=WayType,
|
|
|
|
schema=WayTypeSchema,
|
|
|
|
),
|
2024-12-02 23:40:41 +01:00
|
|
|
],
|
|
|
|
"level": [
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Building,
|
|
|
|
schema=BuildingSchema,
|
|
|
|
filters=LevelGeometryFilter
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Space,
|
|
|
|
schema=SpaceSchema,
|
|
|
|
filters=SpaceFilters,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Door,
|
|
|
|
schema=DoorSchema,
|
|
|
|
filters=LevelGeometryFilter,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
"space": [
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Hole,
|
|
|
|
schema=HoleSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Area,
|
|
|
|
schema=AreaSchema,
|
|
|
|
filters=AreaFilters,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Stair,
|
|
|
|
schema=StairSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Ramp,
|
|
|
|
schema=RampSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Obstacle,
|
|
|
|
schema=ObstacleSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=LineObstacle,
|
|
|
|
schema=LineObstacleSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=Column,
|
|
|
|
schema=ColumnSchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=POI,
|
|
|
|
schema=POISchema,
|
|
|
|
filters=SpaceGeometryFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=LeaveDescription,
|
|
|
|
schema=LeaveDescriptionSchema,
|
|
|
|
filters=BySpaceFilter,
|
|
|
|
),
|
|
|
|
MapdataEndpoint(
|
|
|
|
model=CrossDescription,
|
|
|
|
schema=CrossDescriptionSchema,
|
|
|
|
filters=BySpaceFilter,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
MapdataAPIBuilder(router=mapdata_api_router).build_all_endpoints(mapdata_endpoints)
|
2024-12-26 21:49:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
@mapdata_api_router.post('/dataoverlayfeatures/{id}', summary="update a data overlay feature (including geometries)",
|
2024-12-26 21:50:21 +01:00
|
|
|
response={204: None, **API404.dict(), **auth_permission_responses})
|
2024-12-26 21:49:07 +01:00
|
|
|
def update_data_overlay_feature(request, id: int, parameters: DataOverlayFeatureUpdateSchema):
|
|
|
|
"""
|
|
|
|
update the data overlay feature
|
|
|
|
"""
|
|
|
|
|
|
|
|
feature = get_object_or_404(DataOverlayFeature, id=id)
|
|
|
|
|
2024-12-26 21:50:21 +01:00
|
|
|
if feature.overlay.edit_access_restriction_id is None or feature.overlay.edit_access_restriction_id not in AccessPermission.get_for_request(
|
|
|
|
request):
|
2024-12-26 21:49:07 +01:00
|
|
|
raise APIPermissionDenied('You are not allowed to edit this object.')
|
|
|
|
|
|
|
|
updates = parameters.dict(exclude_unset=True)
|
|
|
|
|
|
|
|
for key, value in updates.items():
|
|
|
|
setattr(feature, key, value)
|
|
|
|
|
|
|
|
feature.save()
|
|
|
|
|
|
|
|
return 204, None
|
|
|
|
|
2024-12-26 21:50:21 +01:00
|
|
|
|
2024-12-26 21:49:07 +01:00
|
|
|
@mapdata_api_router.post('/dataoverlayfeatures-bulk', summary="bulk-update data overlays (including geometries)",
|
|
|
|
response={204: None, **API404.dict(), **auth_permission_responses})
|
|
|
|
def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBulkUpdateSchema):
|
|
|
|
permissions = AccessPermission.get_for_request(request)
|
|
|
|
|
|
|
|
updates = {
|
|
|
|
u.id: u
|
|
|
|
for u in parameters.updates
|
|
|
|
}
|
|
|
|
|
|
|
|
forbidden_object_ids = []
|
|
|
|
|
|
|
|
with transaction.atomic():
|
2024-12-26 21:50:21 +01:00
|
|
|
features = DataOverlayFeature.objects.filter(id__in=updates.keys()).annotate(
|
|
|
|
edit_access_restriction_id=F('overlay__edit_access_restriction_id'))
|
2024-12-26 21:49:07 +01:00
|
|
|
|
|
|
|
for feature in features:
|
|
|
|
if feature.edit_access_restriction_id is None or feature.edit_access_restriction_id not in permissions:
|
|
|
|
forbidden_object_ids.append(feature.id)
|
|
|
|
continue
|
|
|
|
|
|
|
|
changes = updates[feature.id].dict(exclude_unset=True)
|
|
|
|
|
|
|
|
for key, value in changes.items():
|
|
|
|
if key == 'id':
|
|
|
|
continue
|
|
|
|
setattr(feature, key, value)
|
|
|
|
feature.save()
|
|
|
|
|
|
|
|
if len(forbidden_object_ids) > 0:
|
|
|
|
raise APIPermissionDenied('You are not allowed to edit the objects with the following ids: %s.'
|
|
|
|
% ", ".join([str(x) for x in forbidden_object_ids]))
|
|
|
|
|
2024-12-26 21:50:21 +01:00
|
|
|
return 204, None
|