rename newapi to api
This commit is contained in:
parent
caf23d053c
commit
ba4c2b7d0a
18 changed files with 101 additions and 115 deletions
0
src/c3nav/mapdata/api/__init__.py
Normal file
0
src/c3nav/mapdata/api/__init__.py
Normal file
110
src/c3nav/mapdata/api/base.py
Normal file
110
src/c3nav/mapdata/api/base.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
import json
|
||||
from functools import wraps
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Prefetch
|
||||
from django.utils.cache import get_conditional_response
|
||||
from django.utils.http import quote_etag
|
||||
from django.utils.translation import get_language
|
||||
from ninja.decorators import decorate_view
|
||||
|
||||
from c3nav.mapdata.models import AccessRestriction, LocationGroup, MapUpdate
|
||||
from c3nav.mapdata.models.access import AccessPermission
|
||||
from c3nav.mapdata.models.geometry.base import GeometryMixin
|
||||
from c3nav.mapdata.models.locations import SpecificLocation
|
||||
from c3nav.mapdata.utils.cache.local import LocalCacheProxy
|
||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||
|
||||
request_cache = LocalCacheProxy(maxsize=64)
|
||||
|
||||
|
||||
def api_etag(permissions=True, etag_func=AccessPermission.etag_func, base_mapdata=False):
|
||||
|
||||
def outer_wrapper(func):
|
||||
@wraps(func)
|
||||
def outer_wrapped_func(request, *args, **kwargs):
|
||||
response = func(request, *args, **kwargs)
|
||||
if response.status_code == 200:
|
||||
if request._target_etag:
|
||||
response['ETag'] = request._target_etag
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
if request._target_cache_key:
|
||||
request_cache.set(request._target_cache_key, response, 900)
|
||||
return response
|
||||
return outer_wrapped_func
|
||||
|
||||
def inner_wrapper(func):
|
||||
@wraps(func)
|
||||
def inner_wrapped_func(request, *args, **kwargs):
|
||||
# calculate the ETag
|
||||
response_format = "json"
|
||||
raw_etag = '%s:%s:%s' % (response_format, get_language(),
|
||||
(etag_func(request) if permissions else MapUpdate.current_cache_key()))
|
||||
if base_mapdata:
|
||||
raw_etag += ':%d' % request.user_permissions.can_access_base_mapdata
|
||||
etag = quote_etag(raw_etag)
|
||||
|
||||
response = get_conditional_response(request, etag)
|
||||
if response:
|
||||
return response
|
||||
|
||||
request._target_etag = etag
|
||||
|
||||
# calculate the cache key
|
||||
data = {}
|
||||
for name, value in kwargs.items():
|
||||
try:
|
||||
model_dump = value.model_dump
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
value = model_dump()
|
||||
data[name] = value
|
||||
cache_key = 'mapdata:api:%s:%s:%s' % (
|
||||
request.resolver_match.route.replace('/', '-').strip('-'),
|
||||
raw_etag,
|
||||
json.dumps(data, separators=(',', ':'), sort_keys=True, cls=DjangoJSONEncoder),
|
||||
)
|
||||
|
||||
request._target_cache_key = cache_key
|
||||
|
||||
response = request_cache.get(cache_key)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
with GeometryMixin.dont_keep_originals():
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return decorate_view(outer_wrapper)(inner_wrapped_func)
|
||||
return inner_wrapper
|
||||
|
||||
|
||||
def api_stats(stat_name):
|
||||
def wrapper(func):
|
||||
@wraps(func)
|
||||
def wrapped_func(request, *args, **kwargs):
|
||||
response = func(request, *args, **kwargs)
|
||||
if response.status_code < 400 and kwargs:
|
||||
name, value = next(iter(kwargs.items()))
|
||||
for value in api_stats_clean_location_value(value):
|
||||
increment_cache_key('apistats__%s__%s__%s' % (stat_name, name, value))
|
||||
return response
|
||||
return wrapped_func
|
||||
return decorate_view(wrapper)
|
||||
|
||||
|
||||
def optimize_query(qs):
|
||||
if issubclass(qs.model, SpecificLocation):
|
||||
base_qs = LocationGroup.objects.select_related('category')
|
||||
qs = qs.prefetch_related(Prefetch('groups', queryset=base_qs))
|
||||
if issubclass(qs.model, AccessRestriction):
|
||||
qs = qs.prefetch_related('groups')
|
||||
return qs
|
||||
|
||||
|
||||
def api_stats_clean_location_value(value):
|
||||
if isinstance(value, str) and value.startswith('c:'):
|
||||
value = value.split(':')
|
||||
value = 'c:%s:%d:%d' % (value[1], int(float(value[2]) / 3) * 3, int(float(value[3]) / 3) * 3)
|
||||
return (value, 'c:anywhere')
|
||||
return (value, )
|
309
src/c3nav/mapdata/api/map.py
Normal file
309
src/c3nav/mapdata/api/map.py
Normal file
|
@ -0,0 +1,309 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
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 c3nav.api.auth import auth_permission_responses, auth_responses, validate_responses
|
||||
from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidationFailed
|
||||
from c3nav.api.utils import NonEmptyStr
|
||||
from c3nav.mapdata.api.base import api_etag, api_stats
|
||||
from c3nav.mapdata.models import Source
|
||||
from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position
|
||||
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
|
||||
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID
|
||||
from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema,
|
||||
LocationDisplay, SlimListableLocationSchema, SlimLocationSchema)
|
||||
from c3nav.mapdata.schemas.responses import BoundsSchema, LocationGeometry
|
||||
from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request,
|
||||
searchable_locations_for_request, visible_locations_for_request)
|
||||
from c3nav.mapdata.utils.user import can_access_editor
|
||||
|
||||
map_api_router = APIRouter(tags=["map"])
|
||||
|
||||
|
||||
@map_api_router.get('/bounds/', summary="get boundaries",
|
||||
description="get maximum boundaries of everything on the map",
|
||||
response={200: BoundsSchema, **auth_responses})
|
||||
@api_etag(permissions=False)
|
||||
def bounds(request):
|
||||
return {
|
||||
"bounds": Source.max_bounds(),
|
||||
}
|
||||
|
||||
|
||||
class LocationEndpointParameters(Schema):
|
||||
searchable: bool = APIField(
|
||||
False,
|
||||
title='only list searchable locations',
|
||||
description='if set, only searchable locations will be listed'
|
||||
)
|
||||
|
||||
|
||||
def can_access_geometry(request):
|
||||
return True # todo: implementFd
|
||||
|
||||
|
||||
class LocationListFilters(BySearchableFilter, RemoveGeometryFilter):
|
||||
pass
|
||||
|
||||
|
||||
def _location_list(request, detailed: bool, filters: LocationListFilters):
|
||||
# todo: cache, visibility, etc…
|
||||
if filters.searchable:
|
||||
locations = searchable_locations_for_request(request)
|
||||
else:
|
||||
locations = visible_locations_for_request(request).values()
|
||||
|
||||
result = [obj.serialize(detailed=detailed, search=filters.searchable,
|
||||
geometry=filters.geometry and can_access_geometry(request),
|
||||
simple_geometry=True)
|
||||
for obj in locations]
|
||||
return result
|
||||
|
||||
|
||||
@map_api_router.get('/locations/', summary="list locations (slim)",
|
||||
description="Get locations (with most important attributes set)",
|
||||
response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_list(request, filters: Query[LocationListFilters]):
|
||||
return _location_list(request, detailed=False, filters=filters)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/full/', summary="list locations (full)",
|
||||
description="Get locations (with all attributes set)",
|
||||
response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_list_full(request, filters: Query[LocationListFilters]):
|
||||
return _location_list(request, detailed=True, filters=filters)
|
||||
|
||||
|
||||
def _location_retrieve(request, location, detailed: bool, geometry: bool, show_redirects: bool):
|
||||
# todo: cache, visibility, etc…
|
||||
|
||||
if location is None:
|
||||
raise API404()
|
||||
|
||||
if isinstance(location, LocationRedirect):
|
||||
if not show_redirects:
|
||||
return redirect('../' + str(location.target.slug)) # todo: use reverse, make pk and slug both work
|
||||
|
||||
if isinstance(location, (DynamicLocation, Position)):
|
||||
request._target_etag = None
|
||||
request._target_cache_key = None
|
||||
|
||||
return location.serialize(
|
||||
detailed=detailed,
|
||||
geometry=geometry and can_access_geometry(request),
|
||||
simple_geometry=True
|
||||
)
|
||||
|
||||
|
||||
def _location_display(request, location):
|
||||
# todo: cache, visibility, etc…
|
||||
|
||||
if location is None:
|
||||
raise API404()
|
||||
|
||||
if isinstance(location, LocationRedirect):
|
||||
return redirect('../' + str(location.target.slug) + '/details/') # todo: use reverse, make pk+slug work
|
||||
|
||||
result = location.details_display(
|
||||
detailed_geometry=can_access_geometry(request),
|
||||
editor_url=can_access_editor(request)
|
||||
)
|
||||
from pprint import pprint
|
||||
pprint(result)
|
||||
return json.loads(json.dumps(result, cls=DjangoJSONEncoder)) # todo: wtf?? well we need to get rid of lazy strings
|
||||
|
||||
|
||||
def _location_geometry(request, location):
|
||||
# todo: cache, visibility, etc…
|
||||
|
||||
if location is None:
|
||||
raise API404()
|
||||
|
||||
if isinstance(location, LocationRedirect):
|
||||
return redirect('../' + str(location.target.slug) + '/geometry/') # todo: use reverse, make pk+slug work
|
||||
|
||||
return LocationGeometry(
|
||||
id=location.pk,
|
||||
level=getattr(location, 'level_id', None),
|
||||
geometry=location.get_geometry(
|
||||
detailed_geometry=can_access_geometry(request)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ShowRedirects(Schema):
|
||||
show_redirects: bool = APIField(
|
||||
False,
|
||||
name="show redirects",
|
||||
description="whether to show redirects instead of sending a redirect response",
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/{location_id}/', summary="location by ID (slim)",
|
||||
description="Get locations by ID (with all attributes set)",
|
||||
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
||||
@api_stats('location_get')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
request,
|
||||
get_location_by_id_for_request(location_id, request),
|
||||
detailed=False, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/{location_id}/full/', summary="location by ID (full)",
|
||||
description="Get location by ID (with all attributes set)",
|
||||
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
||||
@api_stats('location_get')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_id_full(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
request,
|
||||
get_location_by_id_for_request(location_id, request),
|
||||
detailed=True, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/{location_id}/display/', summary="location display by ID",
|
||||
description="Get location display information by ID",
|
||||
response={200: LocationDisplay, **API404.dict(), **auth_responses})
|
||||
@api_stats('location_display')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_id_display(request, location_id: AnyLocationID):
|
||||
return _location_display(
|
||||
request,
|
||||
get_location_by_id_for_request(location_id, request),
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/{location_id}/geometry/', summary="location geometry by id",
|
||||
description="Get location geometry (if available) by ID",
|
||||
response={200: LocationGeometry, **API404.dict(), **auth_responses})
|
||||
@api_stats('location_geometery')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_id_geometry(request, location_id: AnyLocationID):
|
||||
return _location_geometry(
|
||||
request,
|
||||
get_location_by_id_for_request(location_id, request),
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/by-slug/{location_slug}/', summary="location by slug (slim)",
|
||||
description="Get location by slug (with most important attributes set)",
|
||||
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
||||
@api_stats('location_get')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_slug(request, location_slug: NonEmptyStr, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
request,
|
||||
get_location_by_slug_for_request(location_slug, request),
|
||||
detailed=False, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/by-slug/{location_slug}/full/', summary="location by slug (full)",
|
||||
description="Get location by slug (with all attributes set)",
|
||||
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
||||
@api_stats('location_get')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_slug_full(request, location_slug: NonEmptyStr, filters: Query[RemoveGeometryFilter],
|
||||
redirects: Query[ShowRedirects]):
|
||||
return _location_retrieve(
|
||||
request,
|
||||
get_location_by_slug_for_request(location_slug, request),
|
||||
detailed=True, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/by-slug/{location_slug}/display/', summary="location display by slug",
|
||||
description="Get location display information by slug",
|
||||
response={200: LocationDisplay, **API404.dict(), **auth_responses})
|
||||
@api_stats('location_display')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_slug_display(request, location_slug: NonEmptyStr):
|
||||
return _location_display(
|
||||
request,
|
||||
get_location_by_slug_for_request(location_slug, request),
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/locations/by-slug/{location_slug}/geometry/', summary="location geometry by slug",
|
||||
description="Get location geometry (if available) by slug",
|
||||
response={200: LocationGeometry, **API404.dict(), **auth_responses})
|
||||
@api_stats('location_geometry')
|
||||
@api_etag(base_mapdata=True)
|
||||
def location_by_slug_geometry(request, location_slug: NonEmptyStr):
|
||||
return _location_geometry(
|
||||
request,
|
||||
get_location_by_slug_for_request(location_slug, request),
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.get('/positions/{position_id}/', summary="moving position coordinates",
|
||||
description="get current coordinates of a moving position / dynamic location",
|
||||
response={200: AnyPositionStatusSchema, **API404.dict(), **auth_responses})
|
||||
@api_stats('get_position')
|
||||
def get_position_by_id(request, position_id: AnyPositionID):
|
||||
# no caching for obvious reasons!
|
||||
location = None
|
||||
if isinstance(position_id, int) or position_id.isdigit():
|
||||
location = get_location_by_id_for_request(position_id, request)
|
||||
if not isinstance(location, DynamicLocation):
|
||||
raise API404()
|
||||
if location is None and position_id.startswith('p:'):
|
||||
try:
|
||||
location = Position.objects.get(secret=position_id[2:])
|
||||
except Position.DoesNotExist:
|
||||
raise API404()
|
||||
return location.serialize_position()
|
||||
|
||||
|
||||
class UpdatePositionSchema(Schema):
|
||||
coordinates_id: Optional[CustomLocationID] = APIField(
|
||||
description="coordinates to set the location to or None to unset it"
|
||||
)
|
||||
timeout: Optional[int] = APIField(
|
||||
None,
|
||||
description="timeout for this new location in seconds, or None if not to change it",
|
||||
example=None,
|
||||
)
|
||||
|
||||
|
||||
@map_api_router.put('/positions/{position_id}/', url_name="position-update",
|
||||
summary="set moving position",
|
||||
description="only the string ID for the position secret must be used",
|
||||
response={200: AnyPositionStatusSchema, **API404.dict(), **auth_permission_responses})
|
||||
def set_position(request, position_id: AnyPositionID, update: UpdatePositionSchema):
|
||||
# todo: may an API key do this?
|
||||
if not update.position_id.startswith('p:'):
|
||||
raise API404()
|
||||
try:
|
||||
location = Position.objects.get(secret=update.position_id[2:])
|
||||
except Position.DoesNotExist:
|
||||
raise API404()
|
||||
if location.owner != request.user:
|
||||
raise APIPermissionDenied()
|
||||
|
||||
coordinates = get_location_by_id_for_request(update.coordinates_id, request)
|
||||
if coordinates is None:
|
||||
raise APIRequestValidationFailed('Cant resolve coordinates.')
|
||||
|
||||
location.coordinates_id = update.coordinates_id
|
||||
location.timeout = update.timeout
|
||||
location.last_coordinates_update = timezone.now()
|
||||
location.save()
|
||||
|
||||
return location.serialize_position()
|
494
src/c3nav/mapdata/api/mapdata.py
Normal file
494
src/c3nav/mapdata/api/mapdata.py
Normal file
|
@ -0,0 +1,494 @@
|
|||
from typing import Optional, Sequence, Type
|
||||
|
||||
from django.db.models import Model
|
||||
from ninja import Query
|
||||
from ninja import Router as APIRouter
|
||||
|
||||
from c3nav.api.auth import auth_responses, validate_responses
|
||||
from c3nav.api.exceptions import API404
|
||||
from c3nav.mapdata.api.base import api_etag, optimize_query
|
||||
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
|
||||
Space, Stair)
|
||||
from c3nav.mapdata.models.access import AccessRestriction, AccessRestrictionGroup
|
||||
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
||||
Obstacle, Ramp)
|
||||
from c3nav.mapdata.models.locations import DynamicLocation
|
||||
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema,
|
||||
LevelGeometryFilter, SpaceGeometryFilter)
|
||||
from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema,
|
||||
BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema,
|
||||
DynamicLocationSchema, HoleSchema, LeaveDescriptionSchema, LevelSchema,
|
||||
LineObstacleSchema, LocationGroupCategorySchema, LocationGroupSchema,
|
||||
ObstacleSchema, POISchema, RampSchema, SourceSchema, SpaceSchema, StairSchema)
|
||||
|
||||
mapdata_api_router = APIRouter(tags=["mapdata"])
|
||||
|
||||
|
||||
def mapdata_list_endpoint(request,
|
||||
model: Type[Model],
|
||||
filters: Optional[FilterSchema] = None,
|
||||
order_by: Sequence[str] = ('pk',)):
|
||||
# todo: request permissions based on api key
|
||||
|
||||
# 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:
|
||||
qs = filters.filter_qs(qs)
|
||||
|
||||
# order_by
|
||||
qs = qs.order_by(*order_by)
|
||||
|
||||
# todo: can access geometry… using defer?
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
def mapdata_retrieve_endpoint(request, model: Type[Model], **lookups):
|
||||
try:
|
||||
return optimize_query(
|
||||
model.qs_for_request(request) if hasattr(model, 'qs_for_request') else model.objects.all()
|
||||
).get(**lookups)
|
||||
except model.DoesNotExist:
|
||||
raise API404("%s not found" % model.__name__.lower())
|
||||
|
||||
|
||||
def schema_description(schema):
|
||||
return schema.__doc__.replace("\n ", "\n")
|
||||
|
||||
|
||||
"""
|
||||
Levels
|
||||
"""
|
||||
|
||||
|
||||
class LevelFilters(ByGroupFilter, ByOnTopOfFilter):
|
||||
pass
|
||||
|
||||
|
||||
@mapdata_api_router.get('/levels/', summary="level list",
|
||||
tags=["mapdata-root"], description=schema_description(LevelSchema),
|
||||
response={200: list[LevelSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def level_list(request, filters: Query[LevelFilters]):
|
||||
return mapdata_list_endpoint(request, model=Level, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/levels/{level_id}/', summary="level by ID",
|
||||
tags=["mapdata-root"], description=schema_description(LevelSchema),
|
||||
response={200: LevelSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def level_by_id(request, level_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Level, pk=level_id)
|
||||
|
||||
|
||||
"""
|
||||
Buildings
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/buildings/', summary="building list",
|
||||
tags=["mapdata-level"], description=schema_description(BuildingSchema),
|
||||
response={200: list[BuildingSchema], **validate_responses, **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def building_list(request, filters: Query[LevelGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Building, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/buildings/{building_id}/', summary="building by ID",
|
||||
tags=["mapdata-level"], description=schema_description(BuildingSchema),
|
||||
response={200: BuildingSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def building_by_id(request, building_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Building, pk=building_id)
|
||||
|
||||
|
||||
"""
|
||||
Spaces
|
||||
"""
|
||||
|
||||
|
||||
class SpaceFilters(ByGroupFilter, LevelGeometryFilter):
|
||||
pass
|
||||
|
||||
|
||||
@mapdata_api_router.get('/spaces/', summary="space list",
|
||||
tags=["mapdata-level"], description=schema_description(SpaceSchema),
|
||||
response={200: list[SpaceSchema], **validate_responses, **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def space_list(request, filters: Query[SpaceFilters]):
|
||||
return mapdata_list_endpoint(request, model=Space, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/space/{space_id}/', summary="space by ID",
|
||||
tags=["mapdata-level"], description=schema_description(SpaceSchema),
|
||||
response={200: SpaceSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def space_by_id(request, space_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Space, pk=space_id)
|
||||
|
||||
|
||||
"""
|
||||
Doors
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/doors/', summary="door list",
|
||||
tags=["mapdata-level"], description=schema_description(DoorSchema),
|
||||
response={200: list[DoorSchema], **validate_responses, **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def door_list(request, filters: Query[LevelGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Door, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/doors/{door_id}/', summary="door by ID",
|
||||
tags=["mapdata-level"], description=schema_description(DoorSchema),
|
||||
response={200: DoorSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag(base_mapdata=True)
|
||||
def door_by_id(request, door_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Door, pk=door_id)
|
||||
|
||||
|
||||
"""
|
||||
Holes
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/holes/', summary="hole list",
|
||||
tags=["mapdata-space"], description=schema_description(HoleSchema),
|
||||
response={200: list[HoleSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def hole_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Hole, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/holes/{hole_id}/', summary="hole by ID",
|
||||
tags=["mapdata-space"], description=schema_description(HoleSchema),
|
||||
response={200: HoleSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def hole_by_id(request, hole_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Hole, pk=hole_id)
|
||||
|
||||
|
||||
"""
|
||||
Areas
|
||||
"""
|
||||
|
||||
|
||||
class AreaFilters(ByGroupFilter, SpaceGeometryFilter):
|
||||
pass
|
||||
|
||||
|
||||
@mapdata_api_router.get('/areas/', summary="area list",
|
||||
tags=["mapdata-space"], description=schema_description(AreaSchema),
|
||||
response={200: list[AreaSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def area_list(request, filters: Query[AreaFilters]):
|
||||
return mapdata_list_endpoint(request, model=Area, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/areas/{area_id}/', summary="area by ID",
|
||||
tags=["mapdata-space"], description=schema_description(AreaSchema),
|
||||
response={200: AreaSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def area_by_id(request, area_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Area, pk=area_id)
|
||||
|
||||
|
||||
"""
|
||||
Stairs
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/stairs/', summary="stair list",
|
||||
tags=["mapdata-space"], description=schema_description(StairSchema),
|
||||
response={200: list[StairSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def stair_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Stair, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/stairs/{stair_id}/', summary="stair by ID",
|
||||
tags=["mapdata-space"], description=schema_description(StairSchema),
|
||||
response={200: StairSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def stair_by_id(request, stair_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Stair, pk=stair_id)
|
||||
|
||||
|
||||
"""
|
||||
Ramps
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/ramps/', summary="ramp list",
|
||||
tags=["mapdata-space"], description=schema_description(RampSchema),
|
||||
response={200: list[RampSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def ramp_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Ramp, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/ramps/{ramp_id}/', summary="ramp by ID",
|
||||
tags=["mapdata-space"], description=schema_description(RampSchema),
|
||||
response={200: RampSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def ramp_by_id(request, ramp_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Ramp, pk=ramp_id)
|
||||
|
||||
|
||||
"""
|
||||
Obstacles
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/obstacles/', summary="obstacle list",
|
||||
tags=["mapdata-space"], description=schema_description(ObstacleSchema),
|
||||
response={200: list[ObstacleSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def obstacle_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Obstacle, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/obstacles/{obstacle_id}/', summary="obstacle by ID",
|
||||
tags=["mapdata-space"], description=schema_description(ObstacleSchema),
|
||||
response={200: ObstacleSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def obstacle_by_id(request, obstacle_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Obstacle, pk=obstacle_id)
|
||||
|
||||
|
||||
"""
|
||||
LineObstacles
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/lineobstacles/', summary="line obstacle list",
|
||||
tags=["mapdata-space"], description=schema_description(LineObstacleSchema),
|
||||
response={200: list[LineObstacleSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def lineobstacle_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=LineObstacle, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/lineobstacles/{lineobstacle_id}/', summary="line obstacle by ID",
|
||||
tags=["mapdata-space"], description=schema_description(LineObstacleSchema),
|
||||
response={200: LineObstacleSchema, **API404.dict(), **auth_responses},)
|
||||
@api_etag()
|
||||
def lineobstacle_by_id(request, lineobstacle_id: int):
|
||||
return mapdata_retrieve_endpoint(request, LineObstacle, pk=lineobstacle_id)
|
||||
|
||||
|
||||
"""
|
||||
Columns
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/columns/', summary="column list",
|
||||
tags=["mapdata-space"], description=schema_description(ColumnSchema),
|
||||
response={200: list[ColumnSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def column_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=Column, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/columns/{column_id}/', summary="column by ID",
|
||||
tags=["mapdata-space"], description=schema_description(ColumnSchema),
|
||||
response={200: ColumnSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def column_by_id(request, column_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Column, pk=column_id)
|
||||
|
||||
|
||||
"""
|
||||
POIs
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/pois/', summary="POI list",
|
||||
tags=["mapdata-space"], description=schema_description(POISchema),
|
||||
response={200: list[POISchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def poi_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=POI, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/pois/{poi_id}/', summary="POI by ID",
|
||||
tags=["mapdata-space"], description=schema_description(POISchema),
|
||||
response={200: POISchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def poi_by_id(request, poi_id: int):
|
||||
return mapdata_retrieve_endpoint(request, POI, pk=poi_id)
|
||||
|
||||
|
||||
"""
|
||||
LeaveDescriptions
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/leavedescriptions/', summary="leave description list",
|
||||
tags=["mapdata-space"], description=schema_description(LeaveDescriptionSchema),
|
||||
response={200: list[LeaveDescriptionSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def leavedescription_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=LeaveDescription, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/leavedescriptions/{leavedescription_id}/', summary="leave description by ID",
|
||||
tags=["mapdata-space"], description=schema_description(LeaveDescriptionSchema),
|
||||
response={200: LeaveDescriptionSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def leavedescription_by_id(request, leavedescription_id: int):
|
||||
return mapdata_retrieve_endpoint(request, LeaveDescription, pk=leavedescription_id)
|
||||
|
||||
|
||||
"""
|
||||
CrossDescriptions
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/crossdescriptions/', summary="cross description list",
|
||||
tags=["mapdata-space"], description=schema_description(CrossDescriptionSchema),
|
||||
response={200: list[CrossDescriptionSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def crossdescription_list(request, filters: Query[SpaceGeometryFilter]):
|
||||
return mapdata_list_endpoint(request, model=CrossDescription, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/crossdescriptions/{crossdescription_id}/', summary="cross description by ID",
|
||||
tags=["mapdata-space"], description=schema_description(CrossDescriptionSchema),
|
||||
response={200: CrossDescriptionSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def crossdescription_by_id(request, crossdescription_id: int):
|
||||
return mapdata_retrieve_endpoint(request, CrossDescription, pk=crossdescription_id)
|
||||
|
||||
|
||||
"""
|
||||
LocationGroup
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/locationgroups/', summary="location group list",
|
||||
tags=["mapdata-root"], description=schema_description(LocationGroupSchema),
|
||||
response={200: list[LocationGroupSchema], **validate_responses, **auth_responses})
|
||||
@api_etag()
|
||||
def locationgroup_list(request, filters: Query[ByCategoryFilter]):
|
||||
return mapdata_list_endpoint(request, model=LocationGroup, filters=filters)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/locationgroups/{locationgroup_id}/', summary="location group by ID",
|
||||
tags=["mapdata-root"], description=schema_description(LocationGroupSchema),
|
||||
response={200: LocationGroupSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def locationgroup_by_id(request, locationgroup_id: int):
|
||||
return mapdata_retrieve_endpoint(request, LocationGroup, pk=locationgroup_id)
|
||||
|
||||
|
||||
"""
|
||||
LocationGroupCategories
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/locationgroupcategories/', summary="location group category list",
|
||||
tags=["mapdata-root"], description=schema_description(LocationGroupCategorySchema),
|
||||
response={200: list[LocationGroupCategorySchema], **auth_responses})
|
||||
@api_etag()
|
||||
def locationgroupcategory_list(request):
|
||||
return mapdata_list_endpoint(request, model=LocationGroupCategory)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/locationgroupcategories/{category_id}/', summary="location group category by ID",
|
||||
tags=["mapdata-root"], description=schema_description(LocationGroupCategorySchema),
|
||||
response={200: LocationGroupCategorySchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def locationgroupcategory_by_id(request, category_id: int):
|
||||
return mapdata_retrieve_endpoint(request, LocationGroupCategory, pk=category_id)
|
||||
|
||||
|
||||
"""
|
||||
Sources
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/sources/', summary="source list",
|
||||
tags=["mapdata-root"], description=schema_description(SourceSchema),
|
||||
response={200: list[SourceSchema], **auth_responses})
|
||||
@api_etag()
|
||||
def source_list(request):
|
||||
return mapdata_list_endpoint(request, model=Source)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/sources/{source_id}/', summary="source by ID",
|
||||
tags=["mapdata-root"], description=schema_description(SourceSchema),
|
||||
response={200: SourceSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def source_by_id(request, source_id: int):
|
||||
return mapdata_retrieve_endpoint(request, Source, pk=source_id)
|
||||
|
||||
|
||||
"""
|
||||
AccessRestrictions
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/accessrestrictions/', summary="access restriction list",
|
||||
tags=["mapdata-root"], description=schema_description(AccessRestrictionSchema),
|
||||
response={200: list[AccessRestrictionSchema], **auth_responses})
|
||||
@api_etag()
|
||||
def accessrestriction_list(request):
|
||||
return mapdata_list_endpoint(request, model=AccessRestriction)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/accessrestrictions/{accessrestriction_id}/', summary="access restriction by ID",
|
||||
tags=["mapdata-root"], description=schema_description(AccessRestrictionSchema),
|
||||
response={200: AccessRestrictionSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def accessrestriction_by_id(request, accessrestriction_id: int):
|
||||
return mapdata_retrieve_endpoint(request, AccessRestriction, pk=accessrestriction_id)
|
||||
|
||||
|
||||
"""
|
||||
AccessRestrictionGroups
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/accessrestrictiongroups/', summary="access restriction group list",
|
||||
tags=["mapdata-root"], description=schema_description(AccessRestrictionGroupSchema),
|
||||
response={200: list[AccessRestrictionGroupSchema], **auth_responses})
|
||||
@api_etag()
|
||||
def accessrestrictiongroup_list(request):
|
||||
return mapdata_list_endpoint(request, model=AccessRestrictionGroup)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/accessrestrictiongroups/{group_id}/', summary="access restriction group by ID",
|
||||
tags=["mapdata-root"], description=schema_description(AccessRestrictionGroupSchema),
|
||||
response={200: AccessRestrictionGroupSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def accessrestrictiongroups_by_id(request, group_id: int):
|
||||
return mapdata_retrieve_endpoint(request, AccessRestrictionGroup, pk=group_id)
|
||||
|
||||
|
||||
"""
|
||||
DynamicLocations
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/dynamiclocations/', summary="dynamic location list",
|
||||
tags=["mapdata-root"], description=schema_description(DynamicLocationSchema),
|
||||
response={200: list[DynamicLocationSchema], **auth_responses})
|
||||
@api_etag()
|
||||
def dynamiclocation_list(request):
|
||||
return mapdata_list_endpoint(request, model=DynamicLocation)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/dynamiclocations/{dynamiclocation_id}/', summary="dynamic location by ID",
|
||||
tags=["mapdata-root"], description=schema_description(DynamicLocationSchema),
|
||||
response={200: DynamicLocationSchema, **API404.dict(), **auth_responses})
|
||||
@api_etag()
|
||||
def dynamiclocation_by_id(request, dynamiclocation_id: int):
|
||||
return mapdata_retrieve_endpoint(request, DynamicLocation, pk=dynamiclocation_id)
|
70
src/c3nav/mapdata/api/updates.py
Normal file
70
src/c3nav/mapdata/api/updates.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpResponse
|
||||
from ninja import Router as APIRouter
|
||||
from ninja import Schema
|
||||
from pydantic import PositiveInt
|
||||
|
||||
from c3nav.api.auth import auth_responses
|
||||
from c3nav.api.utils import NonEmptyStr
|
||||
from c3nav.mapdata.api.base import api_etag
|
||||
from c3nav.mapdata.models import MapUpdate
|
||||
from c3nav.mapdata.schemas.responses import BoundsSchema
|
||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||
from c3nav.mapdata.utils.user import get_user_data
|
||||
from c3nav.mapdata.views import set_tile_access_cookie
|
||||
|
||||
updates_api_router = APIRouter(tags=["updates"])
|
||||
|
||||
|
||||
class UserDataSchema(Schema):
|
||||
logged_in: bool
|
||||
allow_editor: bool
|
||||
allow_control_panel: bool
|
||||
has_positions: bool
|
||||
title: NonEmptyStr
|
||||
subtitle: NonEmptyStr
|
||||
permissions: list[PositiveInt]
|
||||
|
||||
|
||||
class FetchUpdatesResponseSchema(Schema):
|
||||
last_site_update: PositiveInt
|
||||
last_map_update: NonEmptyStr
|
||||
user: Optional[UserDataSchema] = None
|
||||
|
||||
|
||||
@updates_api_router.get('/fetch/', summary="fetch updates",
|
||||
description="get regular updates.\n\n"
|
||||
"this endpoint also sets/updates the tile access cookie."
|
||||
"if not called regularly, the tileserver will ignore your access permissions.\n\n"
|
||||
"this endpoint can be called cross-origin, but it will have no user data then.",
|
||||
response={200: FetchUpdatesResponseSchema, **auth_responses})
|
||||
def fetch_updates(request, response: HttpResponse):
|
||||
cross_origin = request.META.get('HTTP_ORIGIN')
|
||||
if cross_origin is not None:
|
||||
try:
|
||||
if request.META['HTTP_HOST'] == urlparse(cross_origin).hostname:
|
||||
cross_origin = None
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
increment_cache_key('api_updates_fetch_requests%s' % ('_cross_origin' if cross_origin is not None else ''))
|
||||
|
||||
from c3nav.site.models import SiteUpdate
|
||||
|
||||
result = {
|
||||
'last_site_update': SiteUpdate.last_update(),
|
||||
'last_map_update': MapUpdate.current_processed_cache_key(),
|
||||
}
|
||||
if cross_origin is None:
|
||||
result.update({
|
||||
'user': get_user_data(request),
|
||||
})
|
||||
|
||||
if cross_origin is not None:
|
||||
response['Access-Control-Allow-Origin'] = cross_origin
|
||||
response['Access-Control-Allow-Credentials'] = 'true'
|
||||
set_tile_access_cookie(request, response)
|
||||
|
||||
return result
|
Loading…
Add table
Add a link
Reference in a new issue