rename newapi to api

This commit is contained in:
Laura Klünder 2023-12-03 21:55:08 +01:00
parent caf23d053c
commit ba4c2b7d0a
18 changed files with 101 additions and 115 deletions

View file

View 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, )

View 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()

View 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)

View 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