goodbye django rest framework
This commit is contained in:
parent
0e65efcbbc
commit
caf23d053c
16 changed files with 48 additions and 1733 deletions
|
@ -1,35 +0,0 @@
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from rest_framework.generics import GenericAPIView
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
|
|
||||||
from c3nav.mapdata.utils.json import json_encoder_reindent
|
|
||||||
|
|
||||||
|
|
||||||
orig_render = JSONRenderer.render
|
|
||||||
|
|
||||||
|
|
||||||
@wraps(JSONRenderer.render)
|
|
||||||
def nicer_renderer(self, data, accepted_media_type=None, renderer_context=None):
|
|
||||||
if self.get_indent(accepted_media_type, renderer_context) is None:
|
|
||||||
return orig_render(self, data, accepted_media_type, renderer_context)
|
|
||||||
shorten_limit = 50
|
|
||||||
if isinstance(data, (list, tuple)):
|
|
||||||
shorten_limit = 5 if any(('geometry' in item) for item in data[:50]) else 50
|
|
||||||
shorten = isinstance(data, (list, tuple)) and len(data) > shorten_limit
|
|
||||||
if shorten:
|
|
||||||
remaining_len = len(data)-shorten_limit
|
|
||||||
data = data[:shorten_limit]
|
|
||||||
result = json_encoder_reindent(lambda d: orig_render(self, d, accepted_media_type, renderer_context), data)
|
|
||||||
if shorten:
|
|
||||||
result = (result[:-2] +
|
|
||||||
('\n ...%d more elements (truncated for HTML preview)...' % remaining_len).encode() +
|
|
||||||
result[-2:])
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# Monkey patch for nicer indentation in the django rest framework
|
|
||||||
JSONRenderer.render = nicer_renderer
|
|
||||||
|
|
||||||
# Fuck serializers!
|
|
||||||
del GenericAPIView.get_serializer
|
|
|
@ -1,91 +0,0 @@
|
||||||
from django.contrib.auth import login, logout
|
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
|
||||||
from django.middleware import csrf
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ViewSet
|
|
||||||
|
|
||||||
from c3nav.api.models import LoginToken
|
|
||||||
from c3nav.api.utils import get_api_post_data
|
|
||||||
|
|
||||||
|
|
||||||
class SessionViewSet(ViewSet):
|
|
||||||
"""
|
|
||||||
Session for Login, Logout, etc…
|
|
||||||
Don't forget to set X-Csrftoken for POST requests!
|
|
||||||
|
|
||||||
/login – POST with fields token or username and password to log in
|
|
||||||
/get_token – POST with fields username and password to get a login token
|
|
||||||
/logout - POST to log out
|
|
||||||
"""
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
return Response({
|
|
||||||
'is_authenticated': request.user.is_authenticated,
|
|
||||||
'csrf_token': csrf.get_token(request),
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
|
||||||
def login(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
raise ParseError(_('Log out first.'))
|
|
||||||
|
|
||||||
data = get_api_post_data(request)
|
|
||||||
|
|
||||||
if 'token' in data:
|
|
||||||
try:
|
|
||||||
token = LoginToken.get_by_token(data['token'])
|
|
||||||
except LoginToken.DoesNotExist:
|
|
||||||
raise PermissionDenied(_('This token does not exist or is no longer valid.'))
|
|
||||||
user = token.user
|
|
||||||
elif 'username' in data:
|
|
||||||
form = AuthenticationForm(request, data=data)
|
|
||||||
if not form.is_valid():
|
|
||||||
raise ParseError(form.errors)
|
|
||||||
user = form.user_cache
|
|
||||||
else:
|
|
||||||
raise ParseError(_('You need to send a token or username and password.'))
|
|
||||||
|
|
||||||
login(request, user)
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'detail': _('Login successful.'),
|
|
||||||
'csrf_token': csrf.get_token(request),
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
|
||||||
def get_token(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
data = get_api_post_data(request)
|
|
||||||
|
|
||||||
form = AuthenticationForm(request, data=data)
|
|
||||||
if not form.is_valid():
|
|
||||||
raise ParseError(form.errors)
|
|
||||||
|
|
||||||
token = form.user_cache.login_tokens.create()
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'token': token.get_token(),
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
|
||||||
def logout(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return ParseError(_('Not logged in.'))
|
|
||||||
|
|
||||||
logout(request)
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'detail': _('Logout successful.'),
|
|
||||||
'csrf_token': csrf.get_token(request),
|
|
||||||
})
|
|
|
@ -1,18 +0,0 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.authentication import TokenAuthentication
|
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
|
||||||
|
|
||||||
|
|
||||||
class APISecretAuthentication(TokenAuthentication):
|
|
||||||
def authenticate_credentials(self, key):
|
|
||||||
try:
|
|
||||||
from c3nav.api.models import Secret
|
|
||||||
secret = Secret.objects.filter(api_secret=key).select_related('user', 'user__permissions')
|
|
||||||
# todo: auth scopes are ignored here, we need to get rid of this
|
|
||||||
except Secret.DoesNotExist:
|
|
||||||
raise AuthenticationFailed(_('Invalid token.'))
|
|
||||||
|
|
||||||
if not secret.user.is_active:
|
|
||||||
raise AuthenticationFailed(_('User inactive or deleted.'))
|
|
||||||
|
|
||||||
return (secret.user, secret)
|
|
|
@ -1,28 +0,0 @@
|
||||||
{% extends "rest_framework/base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
{% load compress %}
|
|
||||||
|
|
||||||
{% block title %}{% if name %}{{ name }} – {% endif %}c3nav API{% endblock %}
|
|
||||||
|
|
||||||
{% block bootstrap_navbar_variant %}{% endblock %}
|
|
||||||
|
|
||||||
{% block meta %}
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
|
||||||
{% if favicon %}
|
|
||||||
<link href="{% static favicon %}" rel="icon">
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block style %}
|
|
||||||
{% compress css %}
|
|
||||||
<link href="{% static 'bootstrap/css/bootstrap.css' %}" rel="stylesheet">
|
|
||||||
<link href="{% static 'rest_framework/css/prettify.css' %}" rel="stylesheet">
|
|
||||||
<link href="{% static 'rest_framework/css/default.css' %}" rel="stylesheet">
|
|
||||||
<link href="{% static 'api/css/style.css' %}" rel="stylesheet">
|
|
||||||
{% endcompress %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block branding %}
|
|
||||||
<span class="navbar-brand">c3nav API</span>
|
|
||||||
{% endblock %}
|
|
|
@ -1,30 +1,13 @@
|
||||||
import inspect
|
from django.urls import path
|
||||||
import re
|
from django.views.generic.base import RedirectView
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from rest_framework.generics import GenericAPIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.routers import SimpleRouter
|
|
||||||
|
|
||||||
from c3nav.api.api import SessionViewSet
|
|
||||||
from c3nav.api.newapi import auth_api_router
|
from c3nav.api.newapi import auth_api_router
|
||||||
from c3nav.api.ninja import ninja_api
|
from c3nav.api.ninja import ninja_api
|
||||||
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
|
||||||
from c3nav.editor.newapi.endpoints import editor_api_router
|
from c3nav.editor.newapi.endpoints import editor_api_router
|
||||||
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
|
||||||
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, DynamicLocationPositionViewSet,
|
|
||||||
HoleViewSet, LeaveDescriptionViewSet, LevelViewSet, LineObstacleViewSet,
|
|
||||||
LocationBySlugViewSet, LocationGroupCategoryViewSet, LocationGroupViewSet,
|
|
||||||
LocationViewSet, MapViewSet, ObstacleViewSet, POIViewSet, RampViewSet, SourceViewSet,
|
|
||||||
SpaceViewSet, StairViewSet, UpdatesViewSet)
|
|
||||||
from c3nav.mapdata.newapi.map import map_api_router
|
from c3nav.mapdata.newapi.map import map_api_router
|
||||||
from c3nav.mapdata.newapi.mapdata import mapdata_api_router
|
from c3nav.mapdata.newapi.mapdata import mapdata_api_router
|
||||||
from c3nav.mapdata.newapi.updates import updates_api_router
|
from c3nav.mapdata.newapi.updates import updates_api_router
|
||||||
from c3nav.mapdata.utils.user import can_access_editor
|
|
||||||
from c3nav.mesh.newapi import mesh_api_router
|
from c3nav.mesh.newapi import mesh_api_router
|
||||||
from c3nav.routing.api import RoutingViewSet
|
|
||||||
from c3nav.routing.newapi.positioning import positioning_api_router
|
from c3nav.routing.newapi.positioning import positioning_api_router
|
||||||
from c3nav.routing.newapi.routing import routing_api_router
|
from c3nav.routing.newapi.routing import routing_api_router
|
||||||
|
|
||||||
|
@ -41,89 +24,8 @@ ninja_api.add_router("/editor/", editor_api_router)
|
||||||
ninja_api.add_router("/mesh/", mesh_api_router)
|
ninja_api.add_router("/mesh/", mesh_api_router)
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
legacy API
|
|
||||||
"""
|
|
||||||
router = SimpleRouter()
|
|
||||||
|
|
||||||
router.register(r'map', MapViewSet, basename='map')
|
|
||||||
router.register(r'levels', LevelViewSet)
|
|
||||||
router.register(r'buildings', BuildingViewSet)
|
|
||||||
router.register(r'spaces', SpaceViewSet)
|
|
||||||
router.register(r'doors', DoorViewSet)
|
|
||||||
router.register(r'holes', HoleViewSet)
|
|
||||||
router.register(r'areas', AreaViewSet)
|
|
||||||
router.register(r'stairs', StairViewSet)
|
|
||||||
router.register(r'ramps', RampViewSet)
|
|
||||||
router.register(r'obstacles', ObstacleViewSet)
|
|
||||||
router.register(r'lineobstacles', LineObstacleViewSet)
|
|
||||||
router.register(r'columns', ColumnViewSet)
|
|
||||||
router.register(r'pois', POIViewSet)
|
|
||||||
router.register(r'leavedescriptions', LeaveDescriptionViewSet)
|
|
||||||
router.register(r'crossdescriptions', CrossDescriptionViewSet)
|
|
||||||
router.register(r'sources', SourceViewSet)
|
|
||||||
router.register(r'accessrestrictions', AccessRestrictionViewSet)
|
|
||||||
router.register(r'accessrestrictiongroups', AccessRestrictionGroupViewSet)
|
|
||||||
|
|
||||||
router.register(r'locations', LocationViewSet)
|
|
||||||
router.register(r'locations/by_slug', LocationBySlugViewSet, basename='location-by-slug')
|
|
||||||
router.register(r'locations/dynamic', DynamicLocationPositionViewSet, basename='dynamic-location')
|
|
||||||
router.register(r'locationgroupcategories', LocationGroupCategoryViewSet)
|
|
||||||
router.register(r'locationgroups', LocationGroupViewSet)
|
|
||||||
|
|
||||||
router.register(r'updates', UpdatesViewSet, basename='updates')
|
|
||||||
|
|
||||||
router.register(r'routing', RoutingViewSet, basename='routing')
|
|
||||||
|
|
||||||
router.register(r'editor', EditorViewSet, basename='editor')
|
|
||||||
router.register(r'changesets', ChangeSetViewSet)
|
|
||||||
router.register(r'session', SessionViewSet, basename='session')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class APIRoot(GenericAPIView):
|
|
||||||
"""
|
|
||||||
Welcome to the c3nav RESTful API.
|
|
||||||
The HTML preview is only shown because your Browser sent text/html in its Accept header.
|
|
||||||
If you want to use this API on a large scale, please use a client that supports E-Tags.
|
|
||||||
For more information on a specific API endpoint, access it with a browser.
|
|
||||||
|
|
||||||
This is the old API which is slowly being phased out in favor of the new API at /api/v2/.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _format_pattern(self, pattern):
|
|
||||||
return re.sub(r'\(\?P<([^>]*[^>_])_?>[^)]+\)', r'{\1}', pattern)[1:-1]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def urls(self):
|
|
||||||
include_editor = can_access_editor(self.request)
|
|
||||||
urls: dict[str, dict[str, str] | str] = OrderedDict()
|
|
||||||
for urlpattern in router.urls:
|
|
||||||
if not include_editor and inspect.getmodule(urlpattern.callback).__name__.startswith('c3nav.editor.'):
|
|
||||||
continue
|
|
||||||
name = urlpattern.name
|
|
||||||
url = self._format_pattern(str(urlpattern.pattern)).replace('{pk}', '{id}')
|
|
||||||
base = url.split('/', 1)[0]
|
|
||||||
if base == 'editor':
|
|
||||||
if name == 'editor-list':
|
|
||||||
continue
|
|
||||||
if name == 'editor-detail':
|
|
||||||
name = 'editor-api'
|
|
||||||
elif base == 'session':
|
|
||||||
if name == 'session-list':
|
|
||||||
name = 'session-info'
|
|
||||||
if '-' in name:
|
|
||||||
urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url
|
|
||||||
else:
|
|
||||||
urls[name] = url
|
|
||||||
return urls
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
return Response(self.urls)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# todo: does this work? can it be better?
|
# todo: does this work? can it be better?
|
||||||
re_path(r'^$', APIRoot.as_view()),
|
path('v2/', ninja_api.urls),
|
||||||
path('', include(router.urls)),
|
path('', RedirectView.as_view(pattern_name="api-v2:openapi-view")),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,683 +0,0 @@
|
||||||
from functools import wraps
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from django.db.models import Prefetch, Q
|
|
||||||
from django.urls import Resolver404, resolve
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.exceptions import NotFound, ParseError, PermissionDenied, ValidationError
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
|
||||||
from shapely import prepared
|
|
||||||
from shapely.ops import unary_union
|
|
||||||
|
|
||||||
from c3nav.api.utils import get_api_post_data
|
|
||||||
from c3nav.editor.forms import ChangeSetForm, RejectForm
|
|
||||||
from c3nav.editor.models import ChangeSet
|
|
||||||
from c3nav.editor.utils import LevelChildEditUtils, SpaceChildEditUtils
|
|
||||||
from c3nav.editor.views.base import editor_etag_func
|
|
||||||
from c3nav.mapdata.api import api_etag
|
|
||||||
from c3nav.mapdata.models import Area, MapUpdate, Source
|
|
||||||
from c3nav.mapdata.models.geometry.space import POI
|
|
||||||
from c3nav.mapdata.utils.geometry import unwrap_geom
|
|
||||||
from c3nav.mapdata.utils.user import can_access_editor
|
|
||||||
|
|
||||||
|
|
||||||
class EditorViewSetMixin(ViewSet):
|
|
||||||
def initial(self, request, *args, **kwargs):
|
|
||||||
if not can_access_editor(request):
|
|
||||||
raise PermissionDenied
|
|
||||||
return super().initial(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def api_etag_with_update_cache_key(**outkwargs):
|
|
||||||
outkwargs.setdefault('cache_kwargs', {})['update_cache_key_match'] = bool
|
|
||||||
|
|
||||||
def wrapper(func):
|
|
||||||
func = api_etag(**outkwargs)(func)
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapped_func(self, request, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
changeset = request.changeset
|
|
||||||
except AttributeError:
|
|
||||||
changeset = ChangeSet.get_for_request(request)
|
|
||||||
request.changeset = changeset
|
|
||||||
|
|
||||||
update_cache_key = request.changeset.raw_cache_key_without_changes
|
|
||||||
update_cache_key_match = request.GET.get('update_cache_key') == update_cache_key
|
|
||||||
return func(self, request, *args,
|
|
||||||
update_cache_key=update_cache_key, update_cache_key_match=update_cache_key_match,
|
|
||||||
**kwargs)
|
|
||||||
return wrapped_func
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class EditorViewSet(EditorViewSetMixin, ViewSet):
|
|
||||||
"""
|
|
||||||
Editor API
|
|
||||||
/geometries/ returns a list of geojson features, you have to specify ?level=<id> or ?space=<id>
|
|
||||||
/geometrystyles/ returns styling information for all geometry types
|
|
||||||
/bounds/ returns the maximum bounds of the map
|
|
||||||
/{path}/ insert an editor path to get an API represantation of it. POST requests on forms are possible as well
|
|
||||||
"""
|
|
||||||
lookup_field = 'path'
|
|
||||||
lookup_value_regex = r'.+'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def space_sorting_func(space):
|
|
||||||
groups = tuple(space.groups.all())
|
|
||||||
if not groups:
|
|
||||||
return (0, 0, 0)
|
|
||||||
return (1, groups[0].category.priority, groups[0].hierarchy, groups[0].priority)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_level_geometries(level):
|
|
||||||
buildings = level.buildings.all()
|
|
||||||
buildings_geom = unary_union([unwrap_geom(building.geometry) for building in buildings])
|
|
||||||
spaces = {space.pk: space for space in level.spaces.all()}
|
|
||||||
holes_geom = []
|
|
||||||
for space in spaces.values():
|
|
||||||
if space.outside:
|
|
||||||
space.geometry = space.geometry.difference(buildings_geom)
|
|
||||||
columns = [column.geometry for column in space.columns.all()]
|
|
||||||
if columns:
|
|
||||||
columns_geom = unary_union([unwrap_geom(column.geometry) for column in space.columns.all()])
|
|
||||||
space.geometry = space.geometry.difference(columns_geom)
|
|
||||||
holes = [unwrap_geom(hole.geometry) for hole in space.holes.all()]
|
|
||||||
if holes:
|
|
||||||
space_holes_geom = unary_union(holes)
|
|
||||||
holes_geom.append(space_holes_geom.intersection(unwrap_geom(space.geometry)))
|
|
||||||
space.geometry = space.geometry.difference(space_holes_geom)
|
|
||||||
|
|
||||||
for building in buildings:
|
|
||||||
building.original_geometry = building.geometry
|
|
||||||
|
|
||||||
if holes_geom:
|
|
||||||
holes_geom = unary_union(holes_geom)
|
|
||||||
holes_geom_prep = prepared.prep(holes_geom)
|
|
||||||
for obj in buildings:
|
|
||||||
if holes_geom_prep.intersects(unwrap_geom(obj.geometry)):
|
|
||||||
obj.geometry = obj.geometry.difference(holes_geom)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
results.extend(buildings)
|
|
||||||
for door in level.doors.all():
|
|
||||||
results.append(door)
|
|
||||||
|
|
||||||
results.extend(sorted(spaces.values(), key=EditorViewSet.space_sorting_func))
|
|
||||||
return results
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_levels_pk(request, level):
|
|
||||||
# noinspection PyPep8Naming
|
|
||||||
Level = request.changeset.wrap_model('Level')
|
|
||||||
levels_under = ()
|
|
||||||
levels_on_top = ()
|
|
||||||
lower_level = level.lower(Level).first()
|
|
||||||
primary_levels = (level,) + ((lower_level,) if lower_level else ())
|
|
||||||
secondary_levels = Level.objects.filter(on_top_of__in=primary_levels).values_list('pk', 'on_top_of')
|
|
||||||
if lower_level:
|
|
||||||
levels_under = tuple(pk for pk, on_top_of in secondary_levels if on_top_of == lower_level.pk)
|
|
||||||
if True:
|
|
||||||
levels_on_top = tuple(pk for pk, on_top_of in secondary_levels if on_top_of == level.pk)
|
|
||||||
levels = chain([level.pk], levels_under, levels_on_top)
|
|
||||||
return levels, levels_on_top, levels_under
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def area_sorting_func(area):
|
|
||||||
groups = tuple(area.groups.all())
|
|
||||||
if not groups:
|
|
||||||
return (0, 0, 0)
|
|
||||||
return (1, groups[0].category.priority, groups[0].hierarchy, groups[0].priority)
|
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag_with_update_cache_key(etag_func=editor_etag_func, cache_parameters={'level': str, 'space': str})
|
|
||||||
def geometries(self, request, update_cache_key, update_cache_key_match, *args, **kwargs):
|
|
||||||
Level = request.changeset.wrap_model('Level')
|
|
||||||
Space = request.changeset.wrap_model('Space')
|
|
||||||
Column = request.changeset.wrap_model('Column')
|
|
||||||
Hole = request.changeset.wrap_model('Hole')
|
|
||||||
AltitudeMarker = request.changeset.wrap_model('AltitudeMarker')
|
|
||||||
Building = request.changeset.wrap_model('Building')
|
|
||||||
Door = request.changeset.wrap_model('Door')
|
|
||||||
LocationGroup = request.changeset.wrap_model('LocationGroup')
|
|
||||||
WifiMeasurement = request.changeset.wrap_model('WifiMeasurement')
|
|
||||||
RangingBeacon = request.changeset.wrap_model('RangingBeacon')
|
|
||||||
|
|
||||||
level = request.GET.get('level')
|
|
||||||
space = request.GET.get('space')
|
|
||||||
if level is not None:
|
|
||||||
if space is not None:
|
|
||||||
raise ValidationError('Only level or space can be specified.')
|
|
||||||
|
|
||||||
level = get_object_or_404(Level.objects.filter(Level.q_for_request(request)), pk=level)
|
|
||||||
|
|
||||||
edit_utils = LevelChildEditUtils(level, request)
|
|
||||||
if not edit_utils.can_access_child_base_mapdata:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
levels, levels_on_top, levels_under = self._get_levels_pk(request, level)
|
|
||||||
# don't prefetch groups for now as changesets do not yet work with m2m-prefetches
|
|
||||||
levels = Level.objects.filter(pk__in=levels).filter(Level.q_for_request(request))
|
|
||||||
# graphnodes_qs = request.changeset.wrap_model('GraphNode').objects.all()
|
|
||||||
levels = levels.prefetch_related(
|
|
||||||
Prefetch('spaces', Space.objects.filter(Space.q_for_request(request)).only(
|
|
||||||
'geometry', 'level', 'outside'
|
|
||||||
)),
|
|
||||||
Prefetch('doors', Door.objects.filter(Door.q_for_request(request)).only('geometry', 'level')),
|
|
||||||
Prefetch('spaces__columns', Column.objects.filter(
|
|
||||||
Q(access_restriction__isnull=True) | ~Column.q_for_request(request)
|
|
||||||
).only('geometry', 'space')),
|
|
||||||
Prefetch('spaces__groups', LocationGroup.objects.only(
|
|
||||||
'color', 'category', 'priority', 'hierarchy', 'category__priority', 'category__allow_spaces'
|
|
||||||
)),
|
|
||||||
Prefetch('buildings', Building.objects.only('geometry', 'level')),
|
|
||||||
Prefetch('spaces__holes', Hole.objects.only('geometry', 'space')),
|
|
||||||
Prefetch('spaces__altitudemarkers', AltitudeMarker.objects.only('geometry', 'space')),
|
|
||||||
Prefetch('spaces__wifi_measurements', WifiMeasurement.objects.only('geometry', 'space')),
|
|
||||||
Prefetch('spaces__ranging_beacons', RangingBeacon.objects.only('geometry', 'space')),
|
|
||||||
# Prefetch('spaces__graphnodes', graphnodes_qs)
|
|
||||||
)
|
|
||||||
|
|
||||||
levels = {s.pk: s for s in levels}
|
|
||||||
|
|
||||||
level = levels[level.pk]
|
|
||||||
levels_under = [levels[pk] for pk in levels_under]
|
|
||||||
levels_on_top = [levels[pk] for pk in levels_on_top]
|
|
||||||
|
|
||||||
# todo: permissions
|
|
||||||
# graphnodes = tuple(chain(*(space.graphnodes.all()
|
|
||||||
# for space in chain(*(level.spaces.all() for level in levels.values())))))
|
|
||||||
# graphnodes_lookup = {node.pk: node for node in graphnodes}
|
|
||||||
|
|
||||||
# graphedges = request.changeset.wrap_model('GraphEdge').objects.all()
|
|
||||||
# graphedges = graphedges.filter(Q(from_node__in=graphnodes) | Q(to_node__in=graphnodes))
|
|
||||||
# graphedges = graphedges.select_related('waytype')
|
|
||||||
|
|
||||||
# this is faster because we only deserialize graphnode geometries once
|
|
||||||
# missing_graphnodes = graphnodes_qs.filter(pk__in=set(chain(*((edge.from_node_id, edge.to_node_id)
|
|
||||||
# for edge in graphedges))))
|
|
||||||
# graphnodes_lookup.update({node.pk: node for node in missing_graphnodes})
|
|
||||||
# for edge in graphedges:
|
|
||||||
# edge._from_node_cache = graphnodes_lookup[edge.from_node_id]
|
|
||||||
# edge._to_node_cache = graphnodes_lookup[edge.to_node_id]
|
|
||||||
|
|
||||||
# graphedges = [edge for edge in graphedges if edge.from_node.space_id != edge.to_node.space_id]
|
|
||||||
|
|
||||||
results = chain(
|
|
||||||
*(self._get_level_geometries(level) for level in levels_under),
|
|
||||||
self._get_level_geometries(level),
|
|
||||||
*(self._get_level_geometries(level) for level in levels_on_top),
|
|
||||||
*(space.altitudemarkers.all() for space in level.spaces.all()),
|
|
||||||
*(space.wifi_measurements.all() for space in level.spaces.all()),
|
|
||||||
*(space.ranging_beacons.all() for space in level.spaces.all()),
|
|
||||||
# graphedges,
|
|
||||||
# graphnodes,
|
|
||||||
)
|
|
||||||
elif space is not None:
|
|
||||||
space_q_for_request = Space.q_for_request(request)
|
|
||||||
qs = Space.objects.filter(space_q_for_request)
|
|
||||||
space = get_object_or_404(qs.select_related('level', 'level__on_top_of'), pk=space)
|
|
||||||
level = space.level
|
|
||||||
|
|
||||||
edit_utils = SpaceChildEditUtils(space, request)
|
|
||||||
if not edit_utils.can_access_child_base_mapdata:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
if request.user_permissions.can_access_base_mapdata:
|
|
||||||
doors = [door for door in level.doors.filter(Door.q_for_request(request)).all()
|
|
||||||
if door.geometry.wrapped_geom.intersects(space.geometry.wrapped_geom)]
|
|
||||||
doors_space_geom = unary_union(
|
|
||||||
[unwrap_geom(door.geometry) for door in doors] +
|
|
||||||
[unwrap_geom(space.geometry)]
|
|
||||||
)
|
|
||||||
|
|
||||||
levels, levels_on_top, levels_under = self._get_levels_pk(request, level.primary_level)
|
|
||||||
if level.on_top_of_id is not None:
|
|
||||||
levels = chain([level.pk], levels_on_top)
|
|
||||||
other_spaces = Space.objects.filter(space_q_for_request, level__pk__in=levels).only(
|
|
||||||
'geometry', 'level'
|
|
||||||
).prefetch_related(
|
|
||||||
Prefetch('groups', LocationGroup.objects.only(
|
|
||||||
'color', 'category', 'priority', 'hierarchy', 'category__priority', 'category__allow_spaces'
|
|
||||||
).filter(color__isnull=False))
|
|
||||||
)
|
|
||||||
|
|
||||||
space = next(s for s in other_spaces if s.pk == space.pk)
|
|
||||||
other_spaces = [s for s in other_spaces
|
|
||||||
if s.geometry.intersects(doors_space_geom) and s.pk != space.pk]
|
|
||||||
all_other_spaces = other_spaces
|
|
||||||
|
|
||||||
if level.on_top_of_id is None:
|
|
||||||
other_spaces_lower = [s for s in other_spaces if s.level_id in levels_under]
|
|
||||||
other_spaces_upper = [s for s in other_spaces if s.level_id in levels_on_top]
|
|
||||||
else:
|
|
||||||
other_spaces_lower = [s for s in other_spaces if s.level_id == level.on_top_of_id]
|
|
||||||
other_spaces_upper = []
|
|
||||||
other_spaces = [s for s in other_spaces if s.level_id == level.pk]
|
|
||||||
|
|
||||||
space.bounds = True
|
|
||||||
|
|
||||||
# deactivated for performance reasons
|
|
||||||
buildings = level.buildings.all()
|
|
||||||
# buildings_geom = unary_union([building.geometry for building in buildings])
|
|
||||||
# for other_space in other_spaces:
|
|
||||||
# if other_space.outside:
|
|
||||||
# other_space.geometry = other_space.geometry.difference(buildings_geom)
|
|
||||||
for other_space in chain(other_spaces, other_spaces_lower, other_spaces_upper):
|
|
||||||
other_space.opacity = 0.4
|
|
||||||
other_space.color = '#ffffff'
|
|
||||||
for building in buildings:
|
|
||||||
building.opacity = 0.5
|
|
||||||
else:
|
|
||||||
buildings = []
|
|
||||||
doors = []
|
|
||||||
other_spaces = []
|
|
||||||
other_spaces_lower = []
|
|
||||||
other_spaces_upper = []
|
|
||||||
all_other_spaces = []
|
|
||||||
|
|
||||||
# todo: permissions
|
|
||||||
if request.user_permissions.can_access_base_mapdata:
|
|
||||||
graphnodes = request.changeset.wrap_model('GraphNode').objects.all()
|
|
||||||
graphnodes = graphnodes.filter((Q(space__in=all_other_spaces)) | Q(space__pk=space.pk))
|
|
||||||
|
|
||||||
space_graphnodes = tuple(node for node in graphnodes if node.space_id == space.pk)
|
|
||||||
|
|
||||||
graphedges = request.changeset.wrap_model('GraphEdge').objects.all()
|
|
||||||
space_graphnodes_ids = tuple(node.pk for node in space_graphnodes)
|
|
||||||
graphedges = graphedges.filter(Q(from_node__pk__in=space_graphnodes_ids) |
|
|
||||||
Q(to_node__pk__in=space_graphnodes_ids))
|
|
||||||
graphedges = graphedges.select_related('from_node', 'to_node', 'waytype').only(
|
|
||||||
'from_node__geometry', 'to_node__geometry', 'waytype__color'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
graphnodes = []
|
|
||||||
graphedges = []
|
|
||||||
|
|
||||||
areas = space.areas.filter(Area.q_for_request(request)).only(
|
|
||||||
'geometry', 'space'
|
|
||||||
).prefetch_related(
|
|
||||||
Prefetch('groups', LocationGroup.objects.order_by(
|
|
||||||
'-category__priority', '-hierarchy', '-priority'
|
|
||||||
).only(
|
|
||||||
'color', 'category', 'priority', 'hierarchy', 'category__priority', 'category__allow_areas'
|
|
||||||
))
|
|
||||||
)
|
|
||||||
for area in areas:
|
|
||||||
area.opacity = 0.5
|
|
||||||
areas = sorted(areas, key=self.area_sorting_func)
|
|
||||||
|
|
||||||
results = chain(
|
|
||||||
buildings,
|
|
||||||
other_spaces_lower,
|
|
||||||
doors,
|
|
||||||
other_spaces,
|
|
||||||
[space],
|
|
||||||
areas,
|
|
||||||
space.holes.all().only('geometry', 'space'),
|
|
||||||
space.stairs.all().only('geometry', 'space'),
|
|
||||||
space.ramps.all().only('geometry', 'space'),
|
|
||||||
space.obstacles.all().only('geometry', 'space', 'color'),
|
|
||||||
space.lineobstacles.all().only('geometry', 'width', 'space', 'color'),
|
|
||||||
space.columns.all().only('geometry', 'space'),
|
|
||||||
space.altitudemarkers.all().only('geometry', 'space'),
|
|
||||||
space.wifi_measurements.all().only('geometry', 'space'),
|
|
||||||
space.ranging_beacons.all().only('geometry', 'space'),
|
|
||||||
space.pois.filter(POI.q_for_request(request)).only('geometry', 'space').prefetch_related(
|
|
||||||
Prefetch('groups', LocationGroup.objects.only(
|
|
||||||
'color', 'category', 'priority', 'hierarchy', 'category__priority', 'category__allow_pois'
|
|
||||||
).filter(color__isnull=False))
|
|
||||||
),
|
|
||||||
other_spaces_upper,
|
|
||||||
graphedges,
|
|
||||||
graphnodes
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValidationError('No level or space specified.')
|
|
||||||
|
|
||||||
return Response(list(chain(
|
|
||||||
[('update_cache_key', update_cache_key)],
|
|
||||||
(self.conditional_geojson(obj, update_cache_key_match) for obj in results)
|
|
||||||
)))
|
|
||||||
|
|
||||||
def conditional_geojson(self, obj, update_cache_key_match):
|
|
||||||
if update_cache_key_match and not obj._affected_by_changeset:
|
|
||||||
return obj.get_geojson_key()
|
|
||||||
|
|
||||||
result = obj.to_geojson(instance=obj)
|
|
||||||
result['properties']['changed'] = obj._affected_by_changeset
|
|
||||||
return result
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(etag_func=MapUpdate.current_cache_key, cache_parameters={})
|
|
||||||
def geometrystyles(self, request, *args, **kwargs):
|
|
||||||
return Response({
|
|
||||||
'building': '#aaaaaa',
|
|
||||||
'space': '#eeeeee',
|
|
||||||
'hole': 'rgba(255, 0, 0, 0.3)',
|
|
||||||
'door': '#ffffff',
|
|
||||||
'area': '#55aaff',
|
|
||||||
'stair': '#a000a0',
|
|
||||||
'ramp': 'rgba(160, 0, 160, 0.2)',
|
|
||||||
'obstacle': '#999999',
|
|
||||||
'lineobstacle': '#999999',
|
|
||||||
'column': 'rgba(0, 0, 50, 0.3)',
|
|
||||||
'poi': '#4488cc',
|
|
||||||
'shadow': '#000000',
|
|
||||||
'graphnode': '#009900',
|
|
||||||
'graphedge': '#00CC00',
|
|
||||||
'altitudemarker': '#0000FF',
|
|
||||||
'wifimeasurement': '#DDDD00',
|
|
||||||
'rangingbeacon': '#CC00CC',
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(etag_func=editor_etag_func, cache_parameters={})
|
|
||||||
def bounds(self, request, *args, **kwargs):
|
|
||||||
return Response({
|
|
||||||
'bounds': Source.max_bounds(),
|
|
||||||
})
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
# allow POST and DELETE methods for the editor API
|
|
||||||
|
|
||||||
if getattr(self, 'get', None).__name__ in ('list', 'retrieve'):
|
|
||||||
if name == 'post' and (self.resolved.url_name.endswith('.create') or
|
|
||||||
self.resolved.url_name.endswith('.edit')):
|
|
||||||
return self.post_or_delete
|
|
||||||
if name == 'delete' and self.resolved.url_name.endswith('.edit'):
|
|
||||||
return self.post_or_delete
|
|
||||||
raise AttributeError
|
|
||||||
|
|
||||||
def post_or_delete(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't automatically do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
return self.retrieve(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
return self.retrieve(request, *args, **kwargs)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def resolved(self):
|
|
||||||
resolved = None
|
|
||||||
path = self.kwargs.get('path', '')
|
|
||||||
if path:
|
|
||||||
try:
|
|
||||||
resolved = resolve('/editor/'+path+'/')
|
|
||||||
except Resolver404:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not resolved:
|
|
||||||
try:
|
|
||||||
resolved = resolve('/editor/'+path)
|
|
||||||
except Resolver404:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.request.sub_resolver_match = resolved
|
|
||||||
|
|
||||||
return resolved
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
resolved = self.resolved
|
|
||||||
if not resolved:
|
|
||||||
raise NotFound(_('No matching editor view endpoint found.'))
|
|
||||||
|
|
||||||
if not getattr(resolved.func, 'api_hybrid', False):
|
|
||||||
raise NotFound(_('Matching editor view point does not provide an API.'))
|
|
||||||
|
|
||||||
get_api_post_data(request)
|
|
||||||
|
|
||||||
response = resolved.func(request, api=True, *resolved.args, **resolved.kwargs)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeSetViewSet(EditorViewSetMixin, ReadOnlyModelViewSet):
|
|
||||||
"""
|
|
||||||
List and manipulate changesets. All lists are ordered by last update descending. Use ?offset= to specify an offset.
|
|
||||||
Don't forget to set X-Csrftoken for POST requests!
|
|
||||||
|
|
||||||
/ lists all changesets this user can see.
|
|
||||||
/user/ lists changesets by this user
|
|
||||||
/reviewing/ lists changesets this user is currently reviewing.
|
|
||||||
/pending_review/ lists changesets this user can review.
|
|
||||||
|
|
||||||
/current/ returns the current changeset.
|
|
||||||
/direct_editing/ POST to activate direct editing (if available).
|
|
||||||
/deactive/ POST to deactivate current changeset or deactivate direct editing
|
|
||||||
|
|
||||||
/{id}/changes/ list all changes of a given changeset.
|
|
||||||
/{id}/activate/ POST to activate given changeset.
|
|
||||||
/{id}/edit/ POST to edit given changeset (provide title and description in POST data).
|
|
||||||
/{id}/restore_object/ POST to restore an object deleted by this changeset (provide change id as id in POST data).
|
|
||||||
/{id}/delete/ POST to delete given changeset.
|
|
||||||
/{id}/propose/ POST to propose given changeset.
|
|
||||||
/{id}/unpropose/ POST to unpropose given changeset.
|
|
||||||
/{id}/review/ POST to review given changeset.
|
|
||||||
/{id}/reject/ POST to reject given changeset (provide reject=1 in POST data for final rejection).
|
|
||||||
/{id}/unreject/ POST to unreject given changeset.
|
|
||||||
/{id}/apply/ POST to accept and apply given changeset.
|
|
||||||
"""
|
|
||||||
queryset = ChangeSet.objects.all()
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return ChangeSet.qs_for_request(self.request).select_related('last_update', 'last_state_update', 'last_change')
|
|
||||||
|
|
||||||
def _list(self, request, qs):
|
|
||||||
offset = 0
|
|
||||||
if 'offset' in request.GET:
|
|
||||||
if not request.GET['offset'].isdigit():
|
|
||||||
raise ParseError('offset has to be a positive integer.')
|
|
||||||
offset = int(request.GET['offset'])
|
|
||||||
return Response([obj.serialize() for obj in qs.order_by('-last_update')[offset:offset+20]])
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
return self._list(request, self.get_queryset())
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def user(self, request, *args, **kwargs):
|
|
||||||
return self._list(request, self.get_queryset().filter(author=request.user))
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def reviewing(self, request, *args, **kwargs):
|
|
||||||
return self._list(request, self.get_queryset().filter(
|
|
||||||
assigned_to=request.user, state='review'
|
|
||||||
))
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def pending_review(self, request, *args, **kwargs):
|
|
||||||
return self._list(request, self.get_queryset().filter(
|
|
||||||
state__in=('proposed', 'reproposed'),
|
|
||||||
))
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
return Response(self.get_object().serialize())
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def current(self, request, *args, **kwargs):
|
|
||||||
changeset = ChangeSet.get_for_request(request)
|
|
||||||
return Response({
|
|
||||||
'direct_editing': changeset.direct_editing,
|
|
||||||
'changeset': changeset.serialize() if changeset.pk else None,
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
|
||||||
def direct_editing(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't automatically do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
if not ChangeSet.can_direct_edit(request):
|
|
||||||
raise PermissionDenied(_('You don\'t have the permission to activate direct editing.'))
|
|
||||||
|
|
||||||
changeset = ChangeSet.get_for_request(request)
|
|
||||||
if changeset.pk is not None:
|
|
||||||
raise PermissionDenied(_('You cannot activate direct editing if you have an active changeset.'))
|
|
||||||
|
|
||||||
request.session['direct_editing'] = True
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'success': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
|
||||||
def deactivate(self, request, *args, **kwargs):
|
|
||||||
# django-rest-framework doesn't automatically do this for logged out requests
|
|
||||||
SessionAuthentication().enforce_csrf(request)
|
|
||||||
|
|
||||||
request.session.pop('changeset', None)
|
|
||||||
request.session['direct_editing'] = False
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'success': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
|
||||||
def changes(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
changeset.fill_changes_cache()
|
|
||||||
return Response([obj.serialize() for obj in changeset.iter_changed_objects()])
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def activate(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_activate(request):
|
|
||||||
raise PermissionDenied(_('You can not activate this change set.'))
|
|
||||||
|
|
||||||
changeset.activate(request)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def edit(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_edit(request):
|
|
||||||
raise PermissionDenied(_('You cannot edit this change set.'))
|
|
||||||
|
|
||||||
form = ChangeSetForm(instance=changeset, data=get_api_post_data(request))
|
|
||||||
if not form.is_valid():
|
|
||||||
raise ParseError(form.errors)
|
|
||||||
|
|
||||||
changeset = form.instance
|
|
||||||
update = changeset.updates.create(user=request.user,
|
|
||||||
title=changeset.title, description=changeset.description)
|
|
||||||
changeset.last_update = update
|
|
||||||
changeset.save()
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def restore_object(self, request, *args, **kwargs):
|
|
||||||
data = get_api_post_data(request)
|
|
||||||
if 'id' not in data:
|
|
||||||
raise ParseError('Missing id.')
|
|
||||||
|
|
||||||
restore_id = data['id']
|
|
||||||
if isinstance(restore_id, str) and restore_id.isdigit():
|
|
||||||
restore_id = int(restore_id)
|
|
||||||
|
|
||||||
if not isinstance(restore_id, int):
|
|
||||||
raise ParseError('id needs to be an integer.')
|
|
||||||
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_edit(request):
|
|
||||||
raise PermissionDenied(_('You can not edit changes on this change set.'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
changed_object = changeset.changed_objects_set.get(pk=restore_id)
|
|
||||||
except Exception:
|
|
||||||
raise NotFound('could not find object.')
|
|
||||||
|
|
||||||
try:
|
|
||||||
changed_object.restore()
|
|
||||||
except PermissionError:
|
|
||||||
raise PermissionDenied(_('You cannot restore this object, because it depends on '
|
|
||||||
'a deleted object or it would violate a unique contraint.'))
|
|
||||||
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def propose(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
raise PermissionDenied(_('You need to log in to propose changes.'))
|
|
||||||
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.title or not changeset.description:
|
|
||||||
raise PermissionDenied(_('You need to add a title an a description to propose this change set.'))
|
|
||||||
|
|
||||||
if not changeset.can_propose(request):
|
|
||||||
raise PermissionDenied(_('You cannot propose this change set.'))
|
|
||||||
|
|
||||||
changeset.propose(request.user)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def unpropose(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_unpropose(request):
|
|
||||||
raise PermissionDenied(_('You cannot unpropose this change set.'))
|
|
||||||
|
|
||||||
changeset.unpropose(request.user)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def review(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_start_review(request):
|
|
||||||
raise PermissionDenied(_('You cannot review these changes.'))
|
|
||||||
|
|
||||||
changeset.start_review(request.user)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def reject(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not not changeset.can_end_review(request):
|
|
||||||
raise PermissionDenied(_('You cannot reject these changes.'))
|
|
||||||
|
|
||||||
form = RejectForm(get_api_post_data(request))
|
|
||||||
if not form.is_valid():
|
|
||||||
raise ParseError(form.errors)
|
|
||||||
|
|
||||||
changeset.reject(request.user, form.cleaned_data['comment'], form.cleaned_data['final'])
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def unreject(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_unreject(request):
|
|
||||||
raise PermissionDenied(_('You cannot unreject these changes.'))
|
|
||||||
|
|
||||||
changeset.unreject(request.user)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def apply(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_end_review(request):
|
|
||||||
raise PermissionDenied(_('You cannot accept and apply these changes.'))
|
|
||||||
|
|
||||||
changeset.apply(request.user)
|
|
||||||
return Response({'success': True})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
changeset = self.get_object()
|
|
||||||
with changeset.lock_to_edit(request) as changeset:
|
|
||||||
if not changeset.can_delete(request):
|
|
||||||
raise PermissionDenied(_('You cannot delete this change set.'))
|
|
||||||
|
|
||||||
changeset.delete()
|
|
||||||
return Response({'success': True})
|
|
|
@ -14,7 +14,6 @@ from django.shortcuts import redirect, render
|
||||||
from django.utils.cache import patch_vary_headers
|
from django.utils.cache import patch_vary_headers
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.response import Response as APIResponse
|
|
||||||
|
|
||||||
from c3nav.editor.models import ChangeSet
|
from c3nav.editor.models import ChangeSet
|
||||||
from c3nav.editor.wrappers import QuerySetWrapper
|
from c3nav.editor.wrappers import QuerySetWrapper
|
||||||
|
@ -269,7 +268,8 @@ def call_api_hybrid_view_for_api(func, request, *args, **kwargs):
|
||||||
result['messages'] = messages
|
result['messages'] = messages
|
||||||
result.move_to_end('messages', last=False)
|
result.move_to_end('messages', last=False)
|
||||||
|
|
||||||
api_response = APIResponse(result, status=response.status_code)
|
# todo: fix this
|
||||||
|
# api_response = APIResponse(result, status=response.status_code)
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
response.add_headers(api_response)
|
response.add_headers(api_response)
|
||||||
return api_response
|
return api_response
|
||||||
|
|
|
@ -1,550 +0,0 @@
|
||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
from collections import namedtuple
|
|
||||||
from functools import wraps
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models import Prefetch
|
|
||||||
from django.http import Http404, HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.utils.cache import get_conditional_response
|
|
||||||
from django.utils.http import http_date, quote_etag, urlsafe_base64_encode
|
|
||||||
from django.utils.translation import get_language
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.exceptions import NotFound, ValidationError
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import GenericViewSet, ReadOnlyModelViewSet, ViewSet
|
|
||||||
|
|
||||||
from c3nav.mapdata.models import AccessRestriction, Building, Door, Hole, LocationGroup, MapUpdate, Source, Space
|
|
||||||
from c3nav.mapdata.models.access import AccessPermission, AccessRestrictionGroup
|
|
||||||
from c3nav.mapdata.models.geometry.base import GeometryMixin
|
|
||||||
from c3nav.mapdata.models.geometry.level import LevelGeometryMixin
|
|
||||||
from c3nav.mapdata.models.geometry.space import (POI, Area, Column, CrossDescription, LeaveDescription, LineObstacle,
|
|
||||||
Obstacle, Ramp, SpaceGeometryMixin, Stair)
|
|
||||||
from c3nav.mapdata.models.level import Level
|
|
||||||
from c3nav.mapdata.models.locations import (DynamicLocation, Location, LocationGroupCategory, LocationRedirect,
|
|
||||||
LocationSlug, Position, SpecificLocation)
|
|
||||||
from c3nav.mapdata.utils.cache.local import LocalCacheProxy
|
|
||||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
|
||||||
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.models import get_submodels
|
|
||||||
from c3nav.mapdata.utils.user import can_access_editor, get_user_data
|
|
||||||
from c3nav.mapdata.views import set_tile_access_cookie
|
|
||||||
|
|
||||||
request_cache = LocalCacheProxy(maxsize=64)
|
|
||||||
|
|
||||||
|
|
||||||
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, )
|
|
||||||
|
|
||||||
|
|
||||||
def api_stats(view_name):
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapped_func(self, request, *args, **kwargs):
|
|
||||||
response = func(self, 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' % (view_name, name, value))
|
|
||||||
return response
|
|
||||||
return wrapped_func
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def api_etag(permissions=True, etag_func=AccessPermission.etag_func,
|
|
||||||
cache_parameters=None, cache_kwargs=None, base_mapdata_check=False):
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapped_func(self, request, *args, **kwargs):
|
|
||||||
response_format = self.perform_content_negotiation(request)[0].format
|
|
||||||
etag_user = (':'+str(request.user.pk or 0)) if response_format == 'api' else ''
|
|
||||||
raw_etag = '%s%s:%s:%s' % (response_format, etag_user, get_language(),
|
|
||||||
(etag_func(request) if permissions else MapUpdate.current_cache_key()))
|
|
||||||
if base_mapdata_check and self.base_mapdata:
|
|
||||||
raw_etag += ':%d' % request.user_permissions.can_access_base_mapdata
|
|
||||||
etag = quote_etag(raw_etag)
|
|
||||||
|
|
||||||
response = get_conditional_response(request, etag=etag)
|
|
||||||
if response is None:
|
|
||||||
cache_key = 'mapdata:api:'+request.path_info[5:].replace('/', '-').strip('-')+':'+raw_etag
|
|
||||||
if cache_parameters is not None:
|
|
||||||
for param, type_ in cache_parameters.items():
|
|
||||||
value = int(param in request.GET) if type_ == bool else type_(request.GET.get(param))
|
|
||||||
cache_key += ':'+urlsafe_base64_encode(str(value).encode())
|
|
||||||
if cache_kwargs is not None:
|
|
||||||
for name, type_ in cache_kwargs.items():
|
|
||||||
value = type_(kwargs[name])
|
|
||||||
if type_ == bool:
|
|
||||||
value = int(value)
|
|
||||||
cache_key += ':'+urlsafe_base64_encode(str(value).encode())
|
|
||||||
data = request_cache.get(cache_key)
|
|
||||||
if data is not None:
|
|
||||||
response = Response(data)
|
|
||||||
|
|
||||||
if response is None:
|
|
||||||
with GeometryMixin.dont_keep_originals():
|
|
||||||
response = func(self, request, *args, **kwargs)
|
|
||||||
if cache_parameters is not None and response.status_code == 200:
|
|
||||||
request_cache.set(cache_key, response.data, 900)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
response['ETag'] = etag
|
|
||||||
response['Cache-Control'] = 'no-cache'
|
|
||||||
return response
|
|
||||||
return wrapped_func
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class MapViewSet(ViewSet):
|
|
||||||
"""
|
|
||||||
Map API
|
|
||||||
/bounds/ returns the maximum bounds of the map
|
|
||||||
"""
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(permissions=False, cache_parameters={})
|
|
||||||
def bounds(self, request, *args, **kwargs):
|
|
||||||
return Response({
|
|
||||||
'bounds': Source.max_bounds(),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class MapdataViewSet(ReadOnlyModelViewSet):
|
|
||||||
base_mapdata = False
|
|
||||||
order_by = ('id', )
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
qs = super().get_queryset()
|
|
||||||
if hasattr(qs.model, 'qs_for_request'):
|
|
||||||
return qs.model.qs_for_request(self.request)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_access_geometry(request, obj):
|
|
||||||
if isinstance(obj, Space):
|
|
||||||
return obj.base_mapdata_accessible or request.user_permissions.can_access_base_mapdata
|
|
||||||
elif isinstance(obj, (Building, Door)):
|
|
||||||
return request.user_permissions.can_access_base_mapdata
|
|
||||||
return True
|
|
||||||
|
|
||||||
qs_filter = namedtuple('qs_filter', ('field', 'model', 'key', 'value'))
|
|
||||||
|
|
||||||
def _get_keys_for_model(self, request, model, key):
|
|
||||||
if hasattr(model, 'qs_for_request'):
|
|
||||||
cache_key = 'mapdata:api:keys:%s:%s:%s' % (model.__name__, key,
|
|
||||||
AccessPermission.cache_key_for_request(request))
|
|
||||||
qs = model.qs_for_request(request)
|
|
||||||
else:
|
|
||||||
cache_key = 'mapdata:api:keys:%s:%s:%s' % (model.__name__, key,
|
|
||||||
MapUpdate.current_cache_key())
|
|
||||||
qs = model.objects.all()
|
|
||||||
|
|
||||||
result = cache.get(cache_key, None)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
|
|
||||||
result = set(qs.values_list(key, flat=True))
|
|
||||||
cache.set(cache_key, result, 300)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_list(self, request):
|
|
||||||
qs = optimize_query(self.get_queryset())
|
|
||||||
filters = []
|
|
||||||
if issubclass(qs.model, LevelGeometryMixin) and 'level' in request.GET:
|
|
||||||
filters.append(self.qs_filter(field='level', model=Level, key='pk', value=request.GET['level']))
|
|
||||||
|
|
||||||
if issubclass(qs.model, SpaceGeometryMixin) and 'space' in request.GET:
|
|
||||||
filters.append(self.qs_filter(field='space', model=Space, key='pk', value=request.GET['space']))
|
|
||||||
|
|
||||||
if issubclass(qs.model, LocationGroup) and 'category' in request.GET:
|
|
||||||
filters.append(self.qs_filter(field='category', model=LocationGroupCategory,
|
|
||||||
key='pk' if request.GET['category'].isdigit() else 'name',
|
|
||||||
value=request.GET['category']))
|
|
||||||
|
|
||||||
if issubclass(qs.model, SpecificLocation) and 'group' in request.GET:
|
|
||||||
filters.append(self.qs_filter(field='groups', model=LocationGroup, key='pk', value=request.GET['group']))
|
|
||||||
|
|
||||||
if qs.model == Level and 'on_top_of' in request.GET:
|
|
||||||
value = None if request.GET['on_top_of'] == 'null' else request.GET['on_top_of']
|
|
||||||
filters.append(self.qs_filter(field='on_top_of', model=Level, key='pk', value=value))
|
|
||||||
|
|
||||||
cache_key = 'mapdata:api:%s:%s' % (qs.model.__name__, AccessPermission.cache_key_for_request(request))
|
|
||||||
for qs_filter in filters:
|
|
||||||
cache_key += ';%s,%s' % (qs_filter.field, qs_filter.value)
|
|
||||||
|
|
||||||
results = cache.get(cache_key, None)
|
|
||||||
if results is not None:
|
|
||||||
return results
|
|
||||||
|
|
||||||
for qs_filter in filters:
|
|
||||||
if qs_filter.key == 'pk' and not qs_filter.value.isdigit():
|
|
||||||
raise ValidationError(detail={
|
|
||||||
'detail': _('%(field)s is not an integer.') % {'field': qs_filter.field}
|
|
||||||
})
|
|
||||||
|
|
||||||
for qs_filter in filters:
|
|
||||||
if qs_filter.value is not None:
|
|
||||||
keys = self._get_keys_for_model(request, qs_filter.model, qs_filter.key)
|
|
||||||
value = int(qs_filter.value) if qs_filter.key == 'pk' else qs_filter.value
|
|
||||||
if value not in keys:
|
|
||||||
raise NotFound(detail=_('%(model)s not found.') % {'model': qs_filter.model._meta.verbose_name})
|
|
||||||
|
|
||||||
results = tuple(qs.order_by(*self.order_by))
|
|
||||||
cache.set(cache_key, results, 300)
|
|
||||||
return results
|
|
||||||
|
|
||||||
@api_etag(base_mapdata_check=True)
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
geometry = 'geometry' in request.GET
|
|
||||||
results = self._get_list(request)
|
|
||||||
if results:
|
|
||||||
geometry = geometry and self.can_access_geometry(request, results[0])
|
|
||||||
|
|
||||||
return Response([obj.serialize(geometry=geometry) for obj in results])
|
|
||||||
|
|
||||||
@api_etag(base_mapdata_check=True)
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
obj = self.get_object()
|
|
||||||
return Response(obj.serialize(geometry=self.can_access_geometry(request, obj)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_types(models_list, **kwargs):
|
|
||||||
return Response([
|
|
||||||
model.serialize_type(**kwargs) for model in models_list
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class LevelViewSet(MapdataViewSet):
|
|
||||||
"""
|
|
||||||
Add ?on_top_of=<null or id> to filter by on_top_of, add ?group=<id> to filter by group.
|
|
||||||
A Level is a Location – so if it is visible, you can use its ID in the Location API as well.
|
|
||||||
"""
|
|
||||||
queryset = Level.objects.all()
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(permissions=False, cache_parameters={})
|
|
||||||
def geometrytypes(self, request):
|
|
||||||
return self.list_types(get_submodels(LevelGeometryMixin))
|
|
||||||
|
|
||||||
|
|
||||||
class BuildingViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries if available, add ?level=<id> to filter by level. """
|
|
||||||
queryset = Building.objects.all()
|
|
||||||
base_mapdata = True
|
|
||||||
|
|
||||||
|
|
||||||
class SpaceViewSet(MapdataViewSet):
|
|
||||||
"""
|
|
||||||
Add ?geometry=1 to get geometries if available, ?level=<id> to filter by level, ?group=<id> to filter by group.
|
|
||||||
A Space is a Location – so if it is visible, you can use its ID in the Location API as well.
|
|
||||||
"""
|
|
||||||
queryset = Space.objects.all()
|
|
||||||
base_mapdata = True
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(permissions=False, cache_parameters={})
|
|
||||||
def geometrytypes(self, request):
|
|
||||||
return self.list_types(get_submodels(SpaceGeometryMixin))
|
|
||||||
|
|
||||||
|
|
||||||
class DoorViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries if available, add ?level=<id> to filter by level. """
|
|
||||||
queryset = Door.objects.all()
|
|
||||||
base_mapdata = True
|
|
||||||
|
|
||||||
|
|
||||||
class HoleViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = Hole.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class AreaViewSet(MapdataViewSet):
|
|
||||||
"""
|
|
||||||
Add ?geometry=1 to get geometries, add ?space=<id> to filter by space, add ?group=<id> to filter by group.
|
|
||||||
An Area is a Location – so if it is visible, you can use its ID in the Location API as well.
|
|
||||||
"""
|
|
||||||
queryset = Area.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class StairViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = Stair.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class RampViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = Ramp.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class ObstacleViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = Obstacle.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LineObstacleViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = LineObstacle.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class ColumnViewSet(MapdataViewSet):
|
|
||||||
""" Add ?geometry=1 to get geometries, add ?space=<id> to filter by space. """
|
|
||||||
queryset = Column.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class POIViewSet(MapdataViewSet):
|
|
||||||
"""
|
|
||||||
Add ?geometry=1 to get geometries, add ?space=<id> to filter by space, add ?group=<id> to filter by group.
|
|
||||||
A POI is a Location – so if it is visible, you can use its ID in the Location API as well.
|
|
||||||
"""
|
|
||||||
queryset = POI.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LeaveDescriptionViewSet(MapdataViewSet):
|
|
||||||
queryset = LeaveDescription.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class CrossDescriptionViewSet(MapdataViewSet):
|
|
||||||
queryset = CrossDescription.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LocationGroupCategoryViewSet(MapdataViewSet):
|
|
||||||
queryset = LocationGroupCategory.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LocationGroupViewSet(MapdataViewSet):
|
|
||||||
"""
|
|
||||||
Add ?category=<id or name> to filter by category.
|
|
||||||
A Location Group is a Location – so if it is visible, you can use its ID in the Location API as well.
|
|
||||||
"""
|
|
||||||
queryset = LocationGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LocationViewSetBase(RetrieveModelMixin, GenericViewSet):
|
|
||||||
queryset = LocationSlug.objects.all()
|
|
||||||
base_mapdata = True
|
|
||||||
|
|
||||||
def get_object(self) -> LocationSlug:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@api_stats('location_retrieve')
|
|
||||||
@api_etag(cache_parameters={'show_redirects': bool, 'detailed': bool, 'geometry': bool}, base_mapdata_check=True)
|
|
||||||
def retrieve(self, request, key=None, *args, **kwargs):
|
|
||||||
show_redirects = 'show_redirects' in request.GET
|
|
||||||
detailed = 'detailed' in request.GET
|
|
||||||
geometry = 'geometry' in request.GET
|
|
||||||
|
|
||||||
location = self.get_object()
|
|
||||||
|
|
||||||
if location is None:
|
|
||||||
raise NotFound
|
|
||||||
|
|
||||||
if isinstance(location, LocationRedirect):
|
|
||||||
if not show_redirects:
|
|
||||||
return redirect('../' + str(location.target.slug)) # todo: why does redirect/reverse not work here?
|
|
||||||
|
|
||||||
return Response(location.serialize(include_type=True, detailed=detailed,
|
|
||||||
geometry=geometry and MapdataViewSet.can_access_geometry(request, location),
|
|
||||||
simple_geometry=True))
|
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
|
||||||
@api_stats('location_details')
|
|
||||||
@api_etag(base_mapdata_check=True)
|
|
||||||
def details(self, request, **kwargs):
|
|
||||||
location = self.get_object()
|
|
||||||
|
|
||||||
if location is None:
|
|
||||||
raise NotFound
|
|
||||||
|
|
||||||
if isinstance(location, LocationRedirect):
|
|
||||||
return redirect('../' + str(location.target.pk) + '/details/')
|
|
||||||
|
|
||||||
return Response(location.details_display(
|
|
||||||
detailed_geometry=MapdataViewSet.can_access_geometry(request, location),
|
|
||||||
editor_url=can_access_editor(request)
|
|
||||||
))
|
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
|
||||||
@api_stats('location_geometry')
|
|
||||||
@api_etag(base_mapdata_check=True)
|
|
||||||
def geometry(self, request, **kwargs):
|
|
||||||
location = self.get_object()
|
|
||||||
|
|
||||||
if location is None:
|
|
||||||
raise NotFound
|
|
||||||
|
|
||||||
if isinstance(location, LocationRedirect):
|
|
||||||
return redirect('../' + str(location.target.pk) + '/geometry/')
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'id': location.pk,
|
|
||||||
'level': getattr(location, 'level_id', None),
|
|
||||||
'geometry': location.get_geometry(
|
|
||||||
detailed_geometry=MapdataViewSet.can_access_geometry(request, location)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class LocationViewSet(LocationViewSetBase):
|
|
||||||
"""
|
|
||||||
Locations are Levels, Spaces, Areas, POIs and Location Groups (see /locations/types/). They have a shared ID pool.
|
|
||||||
This API endpoint only accesses locations that have can_search or can_describe set to true.
|
|
||||||
If you want to access all of them, use the API endpoints for the Location Types.
|
|
||||||
Additionally, you can access Custom Locations (Coordinates) by using c:<level.short_label>:x:y as an id or slug.
|
|
||||||
|
|
||||||
add ?searchable to only show locations with can_search set to true ordered by relevance
|
|
||||||
add ?detailed to show all attributes
|
|
||||||
add ?geometry to show geometries if available
|
|
||||||
/{id}/ add ?show_redirect=1 to suppress redirects and show them as JSON.
|
|
||||||
also possible: /by_slug/{slug}/
|
|
||||||
"""
|
|
||||||
queryset = LocationSlug.objects.all()
|
|
||||||
lookup_value_regex = r'[^/]+'
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
return get_location_by_id_for_request(self.kwargs['pk'], self.request)
|
|
||||||
|
|
||||||
@api_etag(cache_parameters={'searchable': bool, 'detailed': bool, 'geometry': bool}, base_mapdata_check=True)
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
searchable = 'searchable' in request.GET
|
|
||||||
detailed = 'detailed' in request.GET
|
|
||||||
geometry = 'geometry' in request.GET
|
|
||||||
|
|
||||||
cache_key = 'mapdata:api:location:list:%d:%s:%d' % (
|
|
||||||
searchable + detailed*2 + geometry*4,
|
|
||||||
AccessPermission.cache_key_for_request(request),
|
|
||||||
request.user_permissions.can_access_base_mapdata
|
|
||||||
)
|
|
||||||
result = cache.get(cache_key, None)
|
|
||||||
if result is None:
|
|
||||||
if searchable:
|
|
||||||
locations = searchable_locations_for_request(request)
|
|
||||||
else:
|
|
||||||
locations = visible_locations_for_request(request).values()
|
|
||||||
|
|
||||||
result = tuple(obj.serialize(include_type=True, detailed=detailed, search=searchable,
|
|
||||||
geometry=geometry and MapdataViewSet.can_access_geometry(request, obj),
|
|
||||||
simple_geometry=True)
|
|
||||||
for obj in locations)
|
|
||||||
cache.set(cache_key, result, 300)
|
|
||||||
|
|
||||||
return Response(result)
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
@api_etag(permissions=False)
|
|
||||||
def types(self, request):
|
|
||||||
return MapdataViewSet.list_types(get_submodels(Location), geomtype=False)
|
|
||||||
|
|
||||||
|
|
||||||
class LocationBySlugViewSet(LocationViewSetBase):
|
|
||||||
queryset = LocationSlug.objects.all()
|
|
||||||
lookup_field = 'slug'
|
|
||||||
lookup_value_regex = r'[^/]+'
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
return get_location_by_slug_for_request(self.kwargs['slug'], self.request)
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicLocationPositionViewSet(UpdateModelMixin, RetrieveModelMixin, GenericViewSet):
|
|
||||||
queryset = LocationSlug.objects.all()
|
|
||||||
lookup_field = 'slug'
|
|
||||||
lookup_value_regex = r'[^/]+'
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
slug = self.kwargs['slug']
|
|
||||||
if slug.startswith('p:'):
|
|
||||||
return get_object_or_404(Position, secret=slug[2:])
|
|
||||||
if slug.isdigit():
|
|
||||||
location = get_location_by_id_for_request(slug, self.request)
|
|
||||||
if isinstance(location, DynamicLocation):
|
|
||||||
return location
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
@api_stats('dynamic_location_retrieve')
|
|
||||||
def retrieve(self, request, key=None, *args, **kwargs):
|
|
||||||
obj = self.get_object()
|
|
||||||
return Response(obj.serialize_position())
|
|
||||||
|
|
||||||
|
|
||||||
class SourceViewSet(MapdataViewSet):
|
|
||||||
queryset = Source.objects.all()
|
|
||||||
order_by = ('name',)
|
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
|
||||||
def image(self, request, pk=None):
|
|
||||||
return self._image(request, pk=pk)
|
|
||||||
|
|
||||||
def _image(self, request, pk=None):
|
|
||||||
source = self.get_object()
|
|
||||||
last_modified = int(os.path.getmtime(source.filepath))
|
|
||||||
response = get_conditional_response(request, last_modified=last_modified)
|
|
||||||
if response is None:
|
|
||||||
response = HttpResponse(open(source.filepath, 'rb'), content_type=mimetypes.guess_type(source.name)[0])
|
|
||||||
response['Last-Modified'] = http_date(last_modified)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class AccessRestrictionViewSet(MapdataViewSet):
|
|
||||||
queryset = AccessRestriction.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class AccessRestrictionGroupViewSet(MapdataViewSet):
|
|
||||||
queryset = AccessRestrictionGroup.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class UpdatesViewSet(GenericViewSet):
|
|
||||||
"""
|
|
||||||
Get information about recent updates.
|
|
||||||
Get display information about the current user.
|
|
||||||
Set the tile access cookie.
|
|
||||||
The tile access cookie is only valid for 1 minute, so if you are displaying a map, call this endpoint repeatedly.
|
|
||||||
"""
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def fetch(self, request, key=None):
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
|
|
||||||
response = Response(result)
|
|
||||||
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 response
|
|
|
@ -2,15 +2,16 @@ import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.db.models import Prefetch
|
||||||
from django.utils.cache import get_conditional_response
|
from django.utils.cache import get_conditional_response
|
||||||
from django.utils.http import quote_etag
|
from django.utils.http import quote_etag
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from ninja.decorators import decorate_view
|
from ninja.decorators import decorate_view
|
||||||
|
|
||||||
from c3nav.mapdata.api import api_stats_clean_location_value
|
from c3nav.mapdata.models import AccessRestriction, LocationGroup, MapUpdate
|
||||||
from c3nav.mapdata.models import MapUpdate
|
|
||||||
from c3nav.mapdata.models.access import AccessPermission
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
from c3nav.mapdata.models.geometry.base import GeometryMixin
|
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.local import LocalCacheProxy
|
||||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||||
|
|
||||||
|
@ -90,3 +91,34 @@ def newapi_stats(stat_name):
|
||||||
return response
|
return response
|
||||||
return wrapped_func
|
return wrapped_func
|
||||||
return decorate_view(wrapper)
|
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, )
|
||||||
|
|
||||||
|
|
||||||
|
def api_stats(view_name):
|
||||||
|
def wrapper(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped_func(self, request, *args, **kwargs):
|
||||||
|
response = func(self, 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' % (view_name, name, value))
|
||||||
|
return response
|
||||||
|
return wrapped_func
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -60,10 +60,10 @@ def _location_list(request, detailed: bool, filters: LocationListFilters):
|
||||||
else:
|
else:
|
||||||
locations = visible_locations_for_request(request).values()
|
locations = visible_locations_for_request(request).values()
|
||||||
|
|
||||||
result = tuple(obj.serialize(detailed=detailed, search=filters.searchable,
|
result = [obj.serialize(detailed=detailed, search=filters.searchable,
|
||||||
geometry=filters.geometry and can_access_geometry(request),
|
geometry=filters.geometry and can_access_geometry(request),
|
||||||
simple_geometry=True)
|
simple_geometry=True)
|
||||||
for obj in locations)
|
for obj in locations]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,13 @@ from ninja import Router as APIRouter
|
||||||
|
|
||||||
from c3nav.api.exceptions import API404
|
from c3nav.api.exceptions import API404
|
||||||
from c3nav.api.newauth import auth_responses, validate_responses
|
from c3nav.api.newauth import auth_responses, validate_responses
|
||||||
from c3nav.mapdata.api import optimize_query
|
|
||||||
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
|
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
|
||||||
Space, Stair)
|
Space, Stair)
|
||||||
from c3nav.mapdata.models.access import AccessRestriction, AccessRestrictionGroup
|
from c3nav.mapdata.models.access import AccessRestriction, AccessRestrictionGroup
|
||||||
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
||||||
Obstacle, Ramp)
|
Obstacle, Ramp)
|
||||||
from c3nav.mapdata.models.locations import DynamicLocation
|
from c3nav.mapdata.models.locations import DynamicLocation
|
||||||
from c3nav.mapdata.newapi.base import newapi_etag
|
from c3nav.mapdata.newapi.base import newapi_etag, optimize_query
|
||||||
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema,
|
from c3nav.mapdata.schemas.filters import (ByCategoryFilter, ByGroupFilter, ByOnTopOfFilter, FilterSchema,
|
||||||
LevelGeometryFilter, SpaceGeometryFilter)
|
LevelGeometryFilter, SpaceGeometryFilter)
|
||||||
from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema,
|
from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRestrictionSchema, AreaSchema,
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.viewsets import ViewSet
|
|
||||||
|
|
||||||
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.utils.cache.stats import increment_cache_key
|
|
||||||
from c3nav.mapdata.utils.locations import visible_locations_for_request
|
|
||||||
from c3nav.routing.exceptions import LocationUnreachable, NoRouteFound, NotYetRoutable
|
|
||||||
from c3nav.routing.forms import RouteForm
|
|
||||||
from c3nav.routing.locator import Locator
|
|
||||||
from c3nav.routing.models import RouteOptions
|
|
||||||
from c3nav.routing.rangelocator import RangeLocator
|
|
||||||
from c3nav.routing.router import Router
|
|
||||||
|
|
||||||
|
|
||||||
class RoutingViewSet(ViewSet):
|
|
||||||
"""
|
|
||||||
/route/ Get routes.
|
|
||||||
/options/ Get or set route options.
|
|
||||||
/locate/ Wifi locate.
|
|
||||||
|
|
||||||
How to use the /locate/ endpoint:
|
|
||||||
POST visible wifi stations as JSON data like this:
|
|
||||||
{
|
|
||||||
"stations": [
|
|
||||||
{
|
|
||||||
"bssid": "11:22:33:44:55:66",
|
|
||||||
"ssid": "36C3",
|
|
||||||
"level": -55,
|
|
||||||
"frequency": 5500
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
@action(detail=False, methods=['get', 'post'])
|
|
||||||
def route(self, request, *args, **kwargs):
|
|
||||||
params = request.POST if request.method == 'POST' else request.GET
|
|
||||||
form = RouteForm(params, request=request)
|
|
||||||
|
|
||||||
if not form.is_valid():
|
|
||||||
return Response({
|
|
||||||
'errors': form.errors,
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
options = RouteOptions.get_for_request(request)
|
|
||||||
try:
|
|
||||||
options.update(params, ignore_unknown=True)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response({
|
|
||||||
'errors': (str(e), ),
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
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 Response({
|
|
||||||
'error': _('Not yet routable, try again shortly.'),
|
|
||||||
})
|
|
||||||
except LocationUnreachable:
|
|
||||||
return Response({
|
|
||||||
'error': _('Unreachable location.'),
|
|
||||||
})
|
|
||||||
except NoRouteFound:
|
|
||||||
return Response({
|
|
||||||
'error': _('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 Response({
|
|
||||||
'request': {
|
|
||||||
'origin': self.get_request_pk(form.cleaned_data['origin']),
|
|
||||||
'destination': self.get_request_pk(form.cleaned_data['destination']),
|
|
||||||
},
|
|
||||||
'options': options.serialize(),
|
|
||||||
'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 get_request_pk(self, location):
|
|
||||||
return location.slug if isinstance(location, Position) else location.pk
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get', 'post'])
|
|
||||||
def options(self, request, *args, **kwargs):
|
|
||||||
options = RouteOptions.get_for_request(request)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
options.update(request.POST, ignore_unknown=True)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response({
|
|
||||||
'errors': (str(e),),
|
|
||||||
}, status=400)
|
|
||||||
options.save()
|
|
||||||
|
|
||||||
return Response(options.serialize())
|
|
||||||
|
|
||||||
@action(detail=False, methods=('POST', ))
|
|
||||||
def locate(self, request, *args, **kwargs):
|
|
||||||
if isinstance(request.data, list):
|
|
||||||
stations_data = request.data
|
|
||||||
data = {}
|
|
||||||
else:
|
|
||||||
data = request.data
|
|
||||||
if 'stations' not in data:
|
|
||||||
return Response({
|
|
||||||
'errors': (_('stations is missing.'),),
|
|
||||||
}, status=400)
|
|
||||||
stations_data = data['stations']
|
|
||||||
|
|
||||||
try:
|
|
||||||
location = Locator.load().locate(stations_data, permissions=AccessPermission.get_for_request(request))
|
|
||||||
if location is not None:
|
|
||||||
increment_cache_key('apistats__locate__%s' % location.pk)
|
|
||||||
except ValidationError:
|
|
||||||
return Response({
|
|
||||||
'errors': (_('Invalid scan data.'),),
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# if 'set_position' in data and location:
|
|
||||||
# set_position = data['set_position']
|
|
||||||
# if not set_position.startswith('p:'):
|
|
||||||
# return Response({
|
|
||||||
# 'errors': (_('Invalid set_position.'),),
|
|
||||||
# }, status=400)
|
|
||||||
#
|
|
||||||
# try:
|
|
||||||
# position = Position.objects.get(secret=set_position[2:])
|
|
||||||
# except Position.DoesNotExist:
|
|
||||||
# return Response({
|
|
||||||
# 'errors': (_('Invalid set_position.'),),
|
|
||||||
# }, status=400)
|
|
||||||
#
|
|
||||||
# form_data = {
|
|
||||||
# **data,
|
|
||||||
# 'coordinates_id': None if location is None else location.pk,
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# # todo: migrate
|
|
||||||
# # form = PositionAPIUpdateForm(instance=position, data=form_data, request=request)
|
|
||||||
# #
|
|
||||||
# # if not form.is_valid():
|
|
||||||
# # return Response({
|
|
||||||
# # 'errors': form.errors,
|
|
||||||
# # }, status=400)
|
|
||||||
# #
|
|
||||||
# # form.save()
|
|
||||||
|
|
||||||
return Response({'location': None if location is None else location.serialize(simple_geometry=True)})
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def locate_test(self, request):
|
|
||||||
from c3nav.mesh.messages import MeshMessageType
|
|
||||||
from c3nav.mesh.models import MeshNode
|
|
||||||
try:
|
|
||||||
node = MeshNode.objects.prefetch_last_messages(MeshMessageType.LOCATE_RANGE_RESULTS).get(
|
|
||||||
address="d4:f9:8d:2d:0d:f1"
|
|
||||||
)
|
|
||||||
except MeshNode.DoesNotExist:
|
|
||||||
return Response({
|
|
||||||
"location": None
|
|
||||||
})
|
|
||||||
msg = node.last_messages[MeshMessageType.LOCATE_RANGE_RESULTS]
|
|
||||||
|
|
||||||
locator = RangeLocator.load()
|
|
||||||
location = locator.locate(
|
|
||||||
{
|
|
||||||
r.peer: r.distance
|
|
||||||
for r in msg.parsed.ranges
|
|
||||||
if r.distance != 0xFFFF
|
|
||||||
},
|
|
||||||
None
|
|
||||||
)
|
|
||||||
return Response({
|
|
||||||
"ranges": msg.parsed.tojson(msg.parsed)["ranges"],
|
|
||||||
"datetime": msg.datetime,
|
|
||||||
"location": location.serialize(simple_geometry=True) if location else None
|
|
||||||
})
|
|
|
@ -10,11 +10,11 @@ from ninja import Schema
|
||||||
from pydantic import PositiveInt
|
from pydantic import PositiveInt
|
||||||
|
|
||||||
from c3nav.api.exceptions import APIRequestValidationFailed
|
from c3nav.api.exceptions import APIRequestValidationFailed
|
||||||
from c3nav.api.newauth import auth_responses, validate_responses, APITokenAuth
|
from c3nav.api.newauth import APITokenAuth, auth_responses, validate_responses
|
||||||
from c3nav.api.utils import NonEmptyStr
|
from c3nav.api.utils import NonEmptyStr
|
||||||
from c3nav.mapdata.api import api_stats_clean_location_value
|
|
||||||
from c3nav.mapdata.models.access import AccessPermission
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
from c3nav.mapdata.models.locations import Position
|
from c3nav.mapdata.models.locations import Position
|
||||||
|
from c3nav.mapdata.newapi.base import api_stats_clean_location_value
|
||||||
from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D
|
from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D
|
||||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||||
from c3nav.mapdata.utils.locations import visible_locations_for_request
|
from c3nav.mapdata.utils.locations import visible_locations_for_request
|
||||||
|
|
|
@ -302,7 +302,6 @@ INSTALLED_APPS = [
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
'ninja',
|
'ninja',
|
||||||
'c3nav.api',
|
'c3nav.api',
|
||||||
'rest_framework',
|
|
||||||
'c3nav.mapdata',
|
'c3nav.mapdata',
|
||||||
'c3nav.routing',
|
'c3nav.routing',
|
||||||
'c3nav.site',
|
'c3nav.site',
|
||||||
|
@ -372,16 +371,6 @@ USE_I18N = True
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
|
||||||
'c3nav.api.auth.APISecretAuthentication',
|
|
||||||
),
|
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
|
||||||
'rest_framework.permissions.AllowAny',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NINJA_PAGINATION_CLASS = "ninja.pagination.LimitOffsetPagination"
|
NINJA_PAGINATION_CLASS = "ninja.pagination.LimitOffsetPagination"
|
||||||
|
|
||||||
LOCALE_PATHS = (
|
LOCALE_PATHS = (
|
||||||
|
|
|
@ -15,8 +15,7 @@ import c3nav.site.urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('editor/', include(c3nav.editor.urls)),
|
path('editor/', include(c3nav.editor.urls)),
|
||||||
path('api/v2/', c3nav.api.urls.ninja_api.urls),
|
path('api/', include(c3nav.api.urls)),
|
||||||
path('api/', include((c3nav.api.urls, 'api'), namespace='api')),
|
|
||||||
path('map/', include(c3nav.mapdata.urls)),
|
path('map/', include(c3nav.mapdata.urls)),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('control/', include(c3nav.control.urls)),
|
path('control/', include(c3nav.control.urls)),
|
||||||
|
|
|
@ -2,7 +2,6 @@ Django==4.2.7
|
||||||
django-bootstrap3==23.4
|
django-bootstrap3==23.4
|
||||||
django-compressor==4.4
|
django-compressor==4.4
|
||||||
csscompressor==0.9.5
|
csscompressor==0.9.5
|
||||||
djangorestframework==3.14.0
|
|
||||||
django-ninja==1.0.1
|
django-ninja==1.0.1
|
||||||
django-filter==23.4
|
django-filter==23.4
|
||||||
django-environ==0.11.2
|
django-environ==0.11.2
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue