diff --git a/src/c3nav/mapdata/api/base.py b/src/c3nav/mapdata/api/base.py index 8de91440..d70981e5 100644 --- a/src/c3nav/mapdata/api/base.py +++ b/src/c3nav/mapdata/api/base.py @@ -1,7 +1,10 @@ import json +import time from functools import wraps +from typing import Optional from django.conf import settings +from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Prefetch from django.utils.cache import get_conditional_response @@ -19,7 +22,8 @@ from c3nav.mapdata.utils.cache.stats import increment_cache_key request_cache = LocalCacheProxy(maxsize=settings.CACHE_SIZE_API) -def api_etag(permissions=True, quests=False, etag_func=AccessPermission.etag_func, base_mapdata=False): +def api_etag(permissions=True, quests=False, etag_func=AccessPermission.etag_func, base_mapdata=False, + etag_add_key: Optional[tuple[str, str]] = None): def outer_wrapper(func): @wraps(func) @@ -63,9 +67,20 @@ def api_etag(permissions=True, quests=False, etag_func=AccessPermission.etag_fun else: value = model_dump() data[name] = value - cache_key = 'mapdata:api:%s:%s:%s' % ( + + etag_add = '' + if etag_add_key: + etag_add_cache_key = ( + f'mapdata:etag_add:{etag_add_key[1]}:{getattr(kwargs[etag_add_key[0]], etag_add_key[1])}' + ) + etag_add = cache.get(etag_add_cache_key, None) + if etag_add is None: + etag_add = int(time.time()) + cache.set(etag_add_cache_key, etag_add, 300) + cache_key = 'mapdata:api:%s:%s:%s:%s' % ( request.resolver_match.route.replace('/', '-').strip('-'), raw_etag, + etag_add, json.dumps(data, separators=(',', ':'), sort_keys=True, cls=DjangoJSONEncoder), ) diff --git a/src/c3nav/mapdata/api/mapdata.py b/src/c3nav/mapdata/api/mapdata.py index 0f7c839c..9781c527 100644 --- a/src/c3nav/mapdata/api/mapdata.py +++ b/src/c3nav/mapdata/api/mapdata.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import Optional, Sequence, Type, Callable, Any +from django.core.cache import cache from django.db import transaction from django.db.models import Model, F from django.shortcuts import get_object_or_404 @@ -79,6 +80,7 @@ class MapdataEndpoint: schema: Type[BaseSchema] filters: Type[FilterSchema] | None = None no_cache: bool = False + etag_add_key: Optional[str] = None name: Optional[str] = None @property @@ -143,7 +145,9 @@ class MapdataAPIBuilder: list_func.__name__ = f"{endpoint.model_name}_list" if not endpoint.no_cache: - list_func = api_etag()(list_func) + list_func = api_etag( + etag_add_key=(("filters", endpoint.etag_add_key) if endpoint.etag_add_key else None) + )(list_func) self.router.get(f"/{endpoint.endpoint_name}/", summary=f"{endpoint.model_name} list", tags=[f"mapdata-{tag}"], description=schema_description(endpoint.schema), @@ -233,13 +237,13 @@ mapdata_endpoints: dict[str, list[MapdataEndpoint]] = { model=DataOverlayFeature, schema=DataOverlayFeatureSchema, filters=ByOverlayFilter, - no_cache=True, + etag_add_key="overlay", ), MapdataEndpoint( model=DataOverlayFeature, schema=DataOverlayFeatureGeometrySchema, filters=ByOverlayFilter, - no_cache=True, + etag_add_key="overlay", name='dataoverlayfeaturegeometries' ), MapdataEndpoint( @@ -341,6 +345,8 @@ def update_data_overlay_feature(request, id: int, parameters: DataOverlayFeature feature.save() + cache.delete(f'mapdata:etag_add:overlay:{feature.overlay_id}') + return 204, None @@ -356,6 +362,7 @@ def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBul forbidden_object_ids = [] + overlay_ids = set() with transaction.atomic(): features = DataOverlayFeature.objects.filter(id__in=updates.keys()).annotate( edit_access_restriction_id=F('overlay__edit_access_restriction_id')) @@ -371,8 +378,15 @@ def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBul if key == 'id': continue setattr(feature, key, value) + + overlay_ids.add(feature.overlay_id) feature.save() + for pk in overlay_ids: + transaction.on_commit( + lambda: cache.delete(f'mapdata:etag_add:overlay:{pk}') + ) + if len(forbidden_object_ids) > 0: raise APIPermissionDenied('You are not allowed to edit the objects with the following ids: %s.' % ", ".join([str(x) for x in forbidden_object_ids]))