From 5c203a7a2b10bfb5456a26a92ff68294fc4240da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 3 Dec 2023 16:37:05 +0100 Subject: [PATCH] editor geometries now available in new API --- src/c3nav/api/newapi.py | 2 +- src/c3nav/api/ninja.py | 2 +- src/c3nav/editor/api.py | 6 +- src/c3nav/editor/newapi/base.py | 30 ++ src/c3nav/editor/newapi/endpoints.py | 55 +-- src/c3nav/editor/newapi/geometries.py | 323 ++++++++++++++++++ src/c3nav/editor/newapi/schemas.py | 7 +- src/c3nav/editor/views/base.py | 2 +- src/c3nav/editor/views/edit.py | 16 +- .../management/commands/generate_c_types.py | 2 +- src/c3nav/routing/newapi/positioning.py | 3 +- src/c3nav/routing/newapi/routing.py | 8 +- 12 files changed, 411 insertions(+), 45 deletions(-) create mode 100644 src/c3nav/editor/newapi/base.py create mode 100644 src/c3nav/editor/newapi/geometries.py diff --git a/src/c3nav/api/newapi.py b/src/c3nav/api/newapi.py index bb3fbe72..5a592944 100644 --- a/src/c3nav/api/newapi.py +++ b/src/c3nav/api/newapi.py @@ -2,7 +2,7 @@ from django.conf import settings from ninja import Router as APIRouter from ninja import Schema -from c3nav.api.newauth import auth_responses, APIAuthMethod +from c3nav.api.newauth import APIAuthMethod, auth_responses from c3nav.api.utils import NonEmptyStr from c3nav.control.models import UserPermissions diff --git a/src/c3nav/api/ninja.py b/src/c3nav/api/ninja.py index 1e3d6e86..eca1e5c8 100644 --- a/src/c3nav/api/ninja.py +++ b/src/c3nav/api/ninja.py @@ -1,4 +1,4 @@ -from ninja import NinjaAPI, Swagger +from ninja import NinjaAPI, Redoc, Swagger from ninja.operation import Operation from ninja.schema import NinjaGenerateJsonSchema diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index 9df6421d..f0e1e3eb 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -18,7 +18,7 @@ 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 etag_func +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 @@ -136,7 +136,7 @@ class EditorViewSet(EditorViewSetMixin, ViewSet): # noinspection PyPep8Naming @action(detail=False, methods=['get']) - @api_etag_with_update_cache_key(etag_func=etag_func, cache_parameters={'level': str, 'space': str}) + @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') @@ -378,7 +378,7 @@ class EditorViewSet(EditorViewSetMixin, ViewSet): }) @action(detail=False, methods=['get']) - @api_etag(etag_func=etag_func, cache_parameters={}) + @api_etag(etag_func=editor_etag_func, cache_parameters={}) def bounds(self, request, *args, **kwargs): return Response({ 'bounds': Source.max_bounds(), diff --git a/src/c3nav/editor/newapi/base.py b/src/c3nav/editor/newapi/base.py new file mode 100644 index 00000000..8d6df4db --- /dev/null +++ b/src/c3nav/editor/newapi/base.py @@ -0,0 +1,30 @@ +from functools import wraps + +from c3nav.editor.models import ChangeSet +from c3nav.mapdata.models.access import AccessPermission +from c3nav.mapdata.newapi.base import newapi_etag + + +def newapi_etag_with_update_cache_key(permissions=True, etag_func=AccessPermission.etag_func, base_mapdata=False): + + def inner_wrapper(func): + func = newapi_etag(permissions=permissions, etag_func=etag_func, base_mapdata=base_mapdata)(func) + @wraps(func) + def inner_wrapped_func(request, *args, **kwargs): + try: + changeset = request.changeset + except AttributeError: + changeset = ChangeSet.get_for_request(request) + request.changeset = changeset + + request_update_cache_key = kwargs.get("update_cache_key", None) + actual_update_cache_key = changeset.raw_cache_key_without_changes + + kwargs.update({ + "update_cache_key": actual_update_cache_key, + "update_cache_key_match": request_update_cache_key == actual_update_cache_key, + }) + return func(request, *args, **kwargs) + + return inner_wrapped_func + return inner_wrapper \ No newline at end of file diff --git a/src/c3nav/editor/newapi/endpoints.py b/src/c3nav/editor/newapi/endpoints.py index 5ce0007d..fd334b9b 100644 --- a/src/c3nav/editor/newapi/endpoints.py +++ b/src/c3nav/editor/newapi/endpoints.py @@ -1,26 +1,15 @@ -from datetime import datetime -from typing import Annotated, Optional, Literal, Union - -from django.db import IntegrityError, transaction -from ninja import Field as APIField -from ninja import Query from ninja import Router as APIRouter -from ninja import Schema, UploadedFile -from ninja.pagination import paginate -from pydantic import PositiveInt, field_validator -from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed -from c3nav.api.newauth import APITokenAuth, auth_permission_responses, auth_responses, validate_responses -from c3nav.api.schema import GeometrySchema, LineSchema -from c3nav.api.utils import NonEmptyStr -from c3nav.editor.newapi.schemas import GeometryStylesSchema, EditorID, EditorSpaceGeometriesElemSchema, \ - EditorLevelGeometriesElemSchema, UpdateCacheKey +from c3nav.api.exceptions import API404 +from c3nav.api.newauth import APITokenAuth, auth_permission_responses +from c3nav.editor.newapi.base import newapi_etag_with_update_cache_key +from c3nav.editor.newapi.geometries import get_level_geometries_result, get_space_geometries_result +from c3nav.editor.newapi.schemas import (EditorID, EditorLevelGeometriesElemSchema, EditorSpaceGeometriesElemSchema, + GeometryStylesSchema, UpdateCacheKey) +from c3nav.editor.views.base import editor_etag_func from c3nav.mapdata.models import Source from c3nav.mapdata.newapi.base import newapi_etag from c3nav.mapdata.schemas.responses import BoundsSchema -from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage -from c3nav.mesh.messages import MeshMessageType -from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage editor_api_router = APIRouter(tags=["editor"], auth=APITokenAuth(permissions={"editor_access"})) @@ -65,12 +54,22 @@ def geometrystyles(): response={200: list[EditorSpaceGeometriesElemSchema], **API404.dict(), **auth_permission_responses}, openapi_extra={"security": [{"APITokenAuth": ["editor_access"]}]}) -@newapi_etag() # todo: correct? -def space_geometries(space_id: EditorID, update_cache_key: UpdateCacheKey = None): +@newapi_etag_with_update_cache_key(etag_func=editor_etag_func) # todo: correct? +def space_geometries(request, space_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs): """ look. this is a complex mess. there will hopefully be more documentation soon. or a better endpoint. """ - raise NotImplementedError + # newapi_etag_with_update_cache_key does the following, don't let it confuse you: + # - update_cache_key becomes the actual update_cache_key, not the one supplied be the user + # - kwargs has "update_cache_key_match", which is true if update_cache_key matches the one supplied be the user + # this is done so the api etag is correctly generated, as it takes the function arguments into account + return get_space_geometries_result( + request, + space_id=space_id, + update_cache_key=update_cache_key, + update_cache_key_match=kwargs["update_cache_key_match"] + ) + # todo: test the heck out of this @editor_api_router.get('/geometries/level/{level_id}/', summary="get the geometries to display for a level", @@ -78,11 +77,21 @@ def space_geometries(space_id: EditorID, update_cache_key: UpdateCacheKey = None **auth_permission_responses}, openapi_extra={"security": [{"APITokenAuth": ["editor_access"]}]}) @newapi_etag() # todo: correct? -def level_geometries(level_id: EditorID, update_cache_key: UpdateCacheKey = None): +def level_geometries(request, level_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs): """ look. this is a complex mess. there will hopefully be more documentation soon. or a better endpoint. """ - raise NotImplementedError + # newapi_etag_with_update_cache_key does the following, don't let it confuse you: + # - update_cache_key becomes the actual update_cache_key, not the one supplied be the user + # - kwargs has "update_cache_key_match", which is true if update_cache_key matches the one supplied be the user + # this is done so the api etag is correctly generated, as it takes the function arguments into account + return get_level_geometries_result( + request, + level_id=level_id, + update_cache_key=update_cache_key, + update_cache_key_match=kwargs["update_cache_key_match"] + ) + # todo: test the heck out of this # todo: need a way to pass the changeset if it's not a session API key diff --git a/src/c3nav/editor/newapi/geometries.py b/src/c3nav/editor/newapi/geometries.py new file mode 100644 index 00000000..a5b4d93e --- /dev/null +++ b/src/c3nav/editor/newapi/geometries.py @@ -0,0 +1,323 @@ +from dataclasses import dataclass +from itertools import chain +from typing import Sequence + +from django.db.models import Prefetch, Q +from shapely import prepared +from shapely.lib import unary_union + +from c3nav.api.exceptions import API404, APIPermissionDenied +from c3nav.editor.utils import LevelChildEditUtils, SpaceChildEditUtils +from c3nav.mapdata.utils.geometry import unwrap_geom + + +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) + + +def _get_geometries_for_one_level(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=space_sorting_func)) + return results + + +@dataclass(slots=True) +class LevelsForLevel: + levels: Sequence[int] # IDs of all levels to render for this level, in order, including the level itself + levels_on_top: Sequence[int] # IDs of levels that are on top of this level (on_top_of field) + levels_under: Sequence[int] # IDs of the level below this level plus levels on top of it (on_top_of field) + + @classmethod + def for_level(cls, request, level: "Level", special_if_on_top=False): # add typing + # 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 = tuple(chain([level.pk], levels_under, levels_on_top)) + + if special_if_on_top and level.on_top_of_id is not None: + levels = tuple(chain([level.pk], levels_on_top)) + levels_under = (level.on_top_of_id, ) + levels_on_top = () + + return cls( + levels=levels, + levels_under=levels_under, + levels_on_top=levels_on_top, + ) + + +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) + + +def conditional_geojson(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 + + +# noinspection PyPep8Naming +def get_level_geometries_result(request, level_id: int, update_cache_key: str, update_cache_key_match: True): + 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') + + try: + level = Level.objects.filter(Level.q_for_request(request)).get(pk=level_id) + except Level.DoesNotExist: + raise API404('Level not found') + + edit_utils = LevelChildEditUtils(level, request) # todo: what's happening here? + if not edit_utils.can_access_child_base_mapdata: + raise APIPermissionDenied() + + levels_for_level = LevelsForLevel.for_level(request, level) + # don't prefetch groups for now as changesets do not yet work with m2m-prefetches + levels = Level.objects.filter(pk__in=levels_for_level.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_for_level.levels_under] + levels_on_top = [levels[pk] for pk in levels_for_level.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( + *(_get_geometries_for_one_level(level) for level in levels_under), + _get_geometries_for_one_level(level), + *(_get_geometries_for_one_level(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, + ) + + return list(chain( + [('update_cache_key', update_cache_key)], + (conditional_geojson(obj, update_cache_key_match) for obj in results) + )) + + +def get_space_geometries_result(request, space_id: int, update_cache_key: str, update_cache_key_match: bool): + Space = request.changeset.wrap_model('Space') + Area = request.changeset.wrap_model('Area') + POI = request.changeset.wrap_model('POI') + Door = request.changeset.wrap_model('Door') + LocationGroup = request.changeset.wrap_model('LocationGroup') + + space_q_for_request = Space.q_for_request(request) + qs = Space.objects.filter(space_q_for_request) + + try: + space = qs.select_related('level', 'level__on_top_of').get(pk=space_id) + except Space.DoesNotExist: + raise API404('space not found') + + level = space.level + + edit_utils = SpaceChildEditUtils(space, request) + if not edit_utils.can_access_child_base_mapdata: + raise APIPermissionDenied + + 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_for_level = LevelsForLevel.for_level(request, level.primary_level, special_if_on_top=True) + other_spaces = Space.objects.filter(space_q_for_request, level__pk__in=levels_for_level.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 + + other_spaces_lower = [s for s in other_spaces if s.level_id in levels_for_level.levels_under] + other_spaces_upper = [s for s in other_spaces if s.level_id in levels_for_level.levels_on_top] + 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: + graph_nodes = request.changeset.wrap_model('GraphNode').objects.all() + graph_nodes = graph_nodes.filter((Q(space__in=all_other_spaces)) | Q(space__pk=space.pk)) + + space_graph_nodes = tuple(node for node in graph_nodes if node.space_id == space.pk) + + graph_edges = request.changeset.wrap_model('GraphEdge').objects.all() + space_graphnodes_ids = tuple(node.pk for node in space_graph_nodes) + graph_edges = graph_edges.filter(Q(from_node__pk__in=space_graphnodes_ids) | + Q(to_node__pk__in=space_graphnodes_ids)) + graph_edges = graph_edges.select_related('from_node', 'to_node', 'waytype').only( + 'from_node__geometry', 'to_node__geometry', 'waytype__color' + ) + else: + graph_nodes = [] + graph_edges = [] + + 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=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, + graph_edges, + graph_nodes + ) + + return list(chain( + [('update_cache_key', update_cache_key)], + (conditional_geojson(obj, update_cache_key_match) for obj in results) + )) diff --git a/src/c3nav/editor/newapi/schemas.py b/src/c3nav/editor/newapi/schemas.py index 0d42538a..bf228b79 100644 --- a/src/c3nav/editor/newapi/schemas.py +++ b/src/c3nav/editor/newapi/schemas.py @@ -1,9 +1,10 @@ -from typing import Annotated, Union, Literal, Optional +from typing import Annotated, Literal, Optional, Union from ninja import Schema -from pydantic import Field as APIField, PositiveInt +from pydantic import Field as APIField +from pydantic import PositiveInt -from c3nav.api.schema import LineSchema, GeometrySchema +from c3nav.api.schema import GeometrySchema, LineSchema from c3nav.api.utils import NonEmptyStr GeometryStylesSchema = Annotated[ diff --git a/src/c3nav/editor/views/base.py b/src/c3nav/editor/views/base.py index 7eb49258..3ce7ff64 100644 --- a/src/c3nav/editor/views/base.py +++ b/src/c3nav/editor/views/base.py @@ -289,7 +289,7 @@ def call_api_hybrid_view_for_html(func, request, *args, **kwargs): raise NoAPIHybridResponse -def etag_func(request, *args, **kwargs): +def editor_etag_func(request, *args, **kwargs): try: changeset = request.changeset except AttributeError: diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py index c6dde44b..b0394601 100644 --- a/src/c3nav/editor/views/edit.py +++ b/src/c3nav/editor/views/edit.py @@ -17,8 +17,8 @@ from django.views.decorators.http import etag from c3nav.editor.forms import GraphEdgeSettingsForm, GraphEditorActionForm from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils, SpaceChildEditUtils from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse, APIHybridLoginRequiredResponse, - APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse, etag_func, - sidebar_view) + APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse, + editor_etag_func, sidebar_view) from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.utils.user import can_access_editor @@ -40,7 +40,7 @@ def child_model(request, model: typing.Union[str, models.Model], kwargs=None, pa } -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view(api_hybrid=True) def main_index(request): Level = request.changeset.wrap_model('Level') @@ -61,7 +61,7 @@ def main_index(request): }, fields=('can_create_level', 'child_models')) -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view(api_hybrid=True) def level_detail(request, pk): Level = request.changeset.wrap_model('Level') @@ -90,7 +90,7 @@ def level_detail(request, pk): }, fields=('level', 'can_edit_graph', 'can_create_level', 'child_models', 'levels_on_top')) -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view(api_hybrid=True) def space_detail(request, level, pk): Level = request.changeset.wrap_model('Level') @@ -126,7 +126,7 @@ def get_changeset_exceeded(request): return request.user_permissions.max_changeset_changes <= request.changeset.changed_objects_count -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view(api_hybrid=True) def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False): changeset_exceeded = get_changeset_exceeded(request) @@ -412,7 +412,7 @@ def get_visible_spaces_kwargs(model, request): return kwargs -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view(api_hybrid=True) def list_objects(request, model=None, level=None, space=None, explicit_edit=False): resolver_match = getattr(request, 'sub_resolver_match', request.resolver_match) @@ -568,7 +568,7 @@ def connect_nodes(request, active_node, clicked_node, edge_settings_form): messages.success(request, _('Reverse edge overwritten.') if is_reverse else _('Edge overwritten.')) -@etag(etag_func) +@etag(editor_etag_func) @sidebar_view def graph_edit(request, level=None, space=None): if not request.user_permissions.can_access_base_mapdata: diff --git a/src/c3nav/mesh/management/commands/generate_c_types.py b/src/c3nav/mesh/management/commands/generate_c_types.py index 0653ade4..41f768b4 100644 --- a/src/c3nav/mesh/management/commands/generate_c_types.py +++ b/src/c3nav/mesh/management/commands/generate_c_types.py @@ -2,7 +2,7 @@ from dataclasses import fields from django.core.management.base import BaseCommand -from c3nav.mesh.baseformats import normalize_name, StructType +from c3nav.mesh.baseformats import StructType, normalize_name from c3nav.mesh.messages import MeshMessage from c3nav.mesh.utils import indent_c diff --git a/src/c3nav/routing/newapi/positioning.py b/src/c3nav/routing/newapi/positioning.py index 9c61b816..47931ac5 100644 --- a/src/c3nav/routing/newapi/positioning.py +++ b/src/c3nav/routing/newapi/positioning.py @@ -1,7 +1,8 @@ from typing import Annotated, Optional from ninja import Field as APIField -from ninja import Router as APIRouter, Schema +from ninja import Router as APIRouter +from ninja import Schema from pydantic import NegativeInt from c3nav.api.newauth import auth_responses diff --git a/src/c3nav/routing/newapi/routing.py b/src/c3nav/routing/newapi/routing.py index 6c53e0d0..ac1970b3 100644 --- a/src/c3nav/routing/newapi/routing.py +++ b/src/c3nav/routing/newapi/routing.py @@ -1,12 +1,14 @@ from enum import StrEnum -from typing import Annotated, Union, Optional +from typing import Annotated, Optional, Union -from ninja import Router as APIRouter, Schema, Field as APIField +from ninja import Field as APIField +from ninja import Router as APIRouter +from ninja import Schema +from pydantic import PositiveInt from c3nav.api.newauth import auth_responses, validate_responses from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.models import Source -from pydantic import PositiveInt from c3nav.mapdata.schemas.model_base import AnyLocationID, Coordinates3D from c3nav.mapdata.schemas.responses import BoundsSchema