separate overlay feature data and geometries into different api endpoints so they can be cached independently
This commit is contained in:
parent
0e19ce5dac
commit
e5ac1e12df
7 changed files with 154 additions and 66 deletions
|
@ -27,8 +27,9 @@ from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRe
|
|||
LineObstacleSchema, LocationGroupCategorySchema, LocationGroupSchema,
|
||||
ObstacleSchema, POISchema, RampSchema, SourceSchema, SpaceSchema, StairSchema,
|
||||
DataOverlaySchema, DataOverlayFeatureSchema, LocationRedirectSchema,
|
||||
WayTypeSchema,
|
||||
DataOverlayFeatureUpdateSchema, DataOverlayFeatureBulkUpdateSchema)
|
||||
WayTypeSchema, DataOverlayFeatureGeometrySchema,
|
||||
DataOverlayFeatureUpdateSchema, DataOverlayFeatureBulkUpdateSchema,
|
||||
)
|
||||
|
||||
mapdata_api_router = APIRouter(tags=["mapdata"])
|
||||
|
||||
|
@ -77,14 +78,16 @@ class MapdataEndpoint:
|
|||
model: Type[Model]
|
||||
schema: Type[BaseSchema]
|
||||
filters: Type[FilterSchema] | None = None
|
||||
no_cache: bool = False
|
||||
name: Optional[str] = None
|
||||
|
||||
@property
|
||||
def model_name(self):
|
||||
return self.model._meta.model_name
|
||||
|
||||
@property
|
||||
def model_name_plural(self):
|
||||
return self.model._meta.default_related_name
|
||||
def endpoint_name(self):
|
||||
return self.name if self.name is not None else self.model._meta.default_related_name
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -110,7 +113,7 @@ class MapdataAPIBuilder:
|
|||
add_call_params = {}
|
||||
call_param_values = set(add_call_params.values())
|
||||
call_params = (
|
||||
*(f"{name}={name}" for name in set(view_params.keys())-call_param_values),
|
||||
*(f"{name}={name}" for name in set(view_params.keys()) - call_param_values),
|
||||
*(f"{name}={value}" for name, value in add_call_params.items()),
|
||||
)
|
||||
method_code = "\n".join((
|
||||
|
@ -139,12 +142,15 @@ class MapdataAPIBuilder:
|
|||
)
|
||||
list_func.__name__ = f"{endpoint.model_name}_list"
|
||||
|
||||
self.router.get(f"/{endpoint.model_name_plural}/", summary=f"{endpoint.model_name} list",
|
||||
if not endpoint.no_cache:
|
||||
list_func = api_etag()(list_func)
|
||||
|
||||
self.router.get(f"/{endpoint.endpoint_name}/", summary=f"{endpoint.model_name} list",
|
||||
tags=[f"mapdata-{tag}"], description=schema_description(endpoint.schema),
|
||||
response={200: list[endpoint.schema],
|
||||
**(validate_responses if endpoint.filters else {}),
|
||||
**auth_responses})(
|
||||
api_etag()(list_func)
|
||||
list_func
|
||||
)
|
||||
|
||||
def add_by_id_endpoint(self, endpoint: MapdataEndpoint, tag: str):
|
||||
|
@ -160,7 +166,7 @@ class MapdataAPIBuilder:
|
|||
)
|
||||
list_func.__name__ = f"{endpoint.model_name}_by_id"
|
||||
|
||||
self.router.get(f'/{endpoint.model_name_plural}/{{{id_field}}}/', summary=f"{endpoint.model_name} by ID",
|
||||
self.router.get(f'/{endpoint.endpoint_name}/{{{id_field}}}/', summary=f"{endpoint.model_name} by ID",
|
||||
tags=[f"mapdata-{tag}"], description=schema_description(endpoint.schema),
|
||||
response={200: endpoint.schema, **API404.dict(), **auth_responses})(
|
||||
api_etag()(list_func)
|
||||
|
@ -227,6 +233,14 @@ mapdata_endpoints: dict[str, list[MapdataEndpoint]] = {
|
|||
model=DataOverlayFeature,
|
||||
schema=DataOverlayFeatureSchema,
|
||||
filters=ByOverlayFilter,
|
||||
no_cache=True,
|
||||
),
|
||||
MapdataEndpoint(
|
||||
model=DataOverlayFeature,
|
||||
schema=DataOverlayFeatureGeometrySchema,
|
||||
filters=ByOverlayFilter,
|
||||
no_cache=True,
|
||||
name='dataoverlayfeaturegeometries'
|
||||
),
|
||||
MapdataEndpoint(
|
||||
model=WayType,
|
||||
|
@ -304,12 +318,11 @@ mapdata_endpoints: dict[str, list[MapdataEndpoint]] = {
|
|||
],
|
||||
}
|
||||
|
||||
|
||||
MapdataAPIBuilder(router=mapdata_api_router).build_all_endpoints(mapdata_endpoints)
|
||||
|
||||
|
||||
@mapdata_api_router.post('/dataoverlayfeatures/{id}', summary="update a data overlay feature (including geometries)",
|
||||
response={204: None, **API404.dict(), **auth_permission_responses})
|
||||
response={204: None, **API404.dict(), **auth_permission_responses})
|
||||
def update_data_overlay_feature(request, id: int, parameters: DataOverlayFeatureUpdateSchema):
|
||||
"""
|
||||
update the data overlay feature
|
||||
|
@ -317,7 +330,8 @@ def update_data_overlay_feature(request, id: int, parameters: DataOverlayFeature
|
|||
|
||||
feature = get_object_or_404(DataOverlayFeature, id=id)
|
||||
|
||||
if feature.overlay.edit_access_restriction_id is None or feature.overlay.edit_access_restriction_id not in AccessPermission.get_for_request(request):
|
||||
if feature.overlay.edit_access_restriction_id is None or feature.overlay.edit_access_restriction_id not in AccessPermission.get_for_request(
|
||||
request):
|
||||
raise APIPermissionDenied('You are not allowed to edit this object.')
|
||||
|
||||
updates = parameters.dict(exclude_unset=True)
|
||||
|
@ -329,10 +343,10 @@ def update_data_overlay_feature(request, id: int, parameters: DataOverlayFeature
|
|||
|
||||
return 204, None
|
||||
|
||||
|
||||
@mapdata_api_router.post('/dataoverlayfeatures-bulk', summary="bulk-update data overlays (including geometries)",
|
||||
response={204: None, **API404.dict(), **auth_permission_responses})
|
||||
def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBulkUpdateSchema):
|
||||
|
||||
permissions = AccessPermission.get_for_request(request)
|
||||
|
||||
updates = {
|
||||
|
@ -343,7 +357,8 @@ def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBul
|
|||
forbidden_object_ids = []
|
||||
|
||||
with transaction.atomic():
|
||||
features = DataOverlayFeature.objects.filter(id__in=updates.keys()).annotate(edit_access_restriction_id=F('overlay__edit_access_restriction_id'))
|
||||
features = DataOverlayFeature.objects.filter(id__in=updates.keys()).annotate(
|
||||
edit_access_restriction_id=F('overlay__edit_access_restriction_id'))
|
||||
|
||||
for feature in features:
|
||||
if feature.edit_access_restriction_id is None or feature.edit_access_restriction_id not in permissions:
|
||||
|
@ -362,4 +377,4 @@ def update_data_overlay_features_bulk(request, parameters: DataOverlayFeatureBul
|
|||
raise APIPermissionDenied('You are not allowed to edit the objects with the following ids: %s.'
|
||||
% ", ".join([str(x) for x in forbidden_object_ids]))
|
||||
|
||||
return 204, None
|
||||
return 204, None
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.1.3 on 2024-12-26 19:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mapdata', '0130_dataoverlay_edit_access_restriction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dataoverlay',
|
||||
name='update_interval',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='in seconds', null=True, verbose_name='frontend update interval'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dataoverlay',
|
||||
name='fill_opacity',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='default fill opacity'),
|
||||
),
|
||||
]
|
|
@ -24,16 +24,21 @@ class DataOverlay(TitledMixin, AccessRestrictionMixin, models.Model):
|
|||
stroke_width = models.FloatField(blank=True, null=True, verbose_name=_('default stroke width'))
|
||||
stroke_opacity = models.FloatField(blank=True, null=True, verbose_name=_('stroke opacity'))
|
||||
fill_color = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('default fill color'))
|
||||
fill_opacity = models.FloatField(blank=True, null=True, verbose_name=_('fill opacity'))
|
||||
fill_opacity = models.FloatField(blank=True, null=True, verbose_name=_('default fill opacity'))
|
||||
|
||||
cluster_points = models.BooleanField(default=False, verbose_name=_('cluster points together when zoomed out'))
|
||||
|
||||
default_geomtype = models.CharField(max_length=255, blank=True, null=True, choices=GeometryType, verbose_name=_('default geometry type'))
|
||||
default_geomtype = models.CharField(max_length=255, blank=True, null=True, choices=GeometryType,
|
||||
verbose_name=_('default geometry type'))
|
||||
|
||||
pull_url = models.URLField(blank=True, null=True, verbose_name=_('pull URL'))
|
||||
pull_headers: dict[str, str] = SchemaField(schema=dict[str, str], null=True,
|
||||
verbose_name=_('headers for pull http request (JSON object)'))
|
||||
pull_interval = models.DurationField(blank=True, null=True, verbose_name=_('pull interval'))
|
||||
|
||||
update_interval = models.PositiveIntegerField(blank=True, null=True, verbose_name=_('frontend update interval'),
|
||||
help_text=_('in seconds'))
|
||||
|
||||
edit_access_restriction = models.ForeignKey(AccessRestriction, null=True, blank=True,
|
||||
related_name='edit_access_restrictions',
|
||||
verbose_name=_('Editor Access Restriction'),
|
||||
|
@ -46,7 +51,8 @@ class DataOverlay(TitledMixin, AccessRestrictionMixin, models.Model):
|
|||
|
||||
|
||||
class DataOverlayFeature(TitledMixin, LevelGeometryMixin, models.Model):
|
||||
overlay = models.ForeignKey('mapdata.DataOverlay', on_delete=models.CASCADE, verbose_name=_('Overlay'), related_name='features')
|
||||
overlay = models.ForeignKey('mapdata.DataOverlay', on_delete=models.CASCADE, verbose_name=_('Overlay'),
|
||||
related_name='features')
|
||||
geometry = GeometryField()
|
||||
# level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level'), related_name='data_overlay_features')
|
||||
external_url = models.URLField(blank=True, null=True, verbose_name=_('external URL'))
|
||||
|
@ -62,9 +68,10 @@ class DataOverlayFeature(TitledMixin, LevelGeometryMixin, models.Model):
|
|||
point_icon = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('point icon'),
|
||||
help_text=_(
|
||||
'use this material icon to display points, instead of drawing a small circle (only makes sense if the geometry is a point)'))
|
||||
extra_data: Optional[dict[str, str|int|bool]] = SchemaField(schema=dict[str, str|int|bool], blank=True, null=True,
|
||||
default=None,
|
||||
verbose_name=_('extra data (JSON object)'))
|
||||
extra_data: Optional[dict[str, str | int | bool]] = SchemaField(schema=dict[str, str | int | bool], blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('extra data (JSON object)'))
|
||||
|
||||
def to_geojson(self, instance=None) -> dict:
|
||||
result = {
|
||||
|
|
|
@ -376,14 +376,13 @@ class DataOverlaySchema(TitledSchema, DjangoModelSchema):
|
|||
fill_color: Optional[str]
|
||||
fill_opacity: Optional[float]
|
||||
cluster_points: bool
|
||||
update_interval: Optional[PositiveInt]
|
||||
|
||||
|
||||
|
||||
class DataOverlayFeatureSchema(TitledSchema, WithGeometrySchema, DjangoModelSchema):
|
||||
class DataOverlayFeatureSchema(TitledSchema, DjangoModelSchema):
|
||||
"""
|
||||
A feature (any kind of geometry) to be displayed as part of a data overlay.
|
||||
"""
|
||||
geometry: AnyGeometrySchema
|
||||
level_id: PositiveInt
|
||||
stroke_color: Optional[str]
|
||||
stroke_width: Optional[float]
|
||||
|
@ -397,6 +396,13 @@ class DataOverlayFeatureSchema(TitledSchema, WithGeometrySchema, DjangoModelSche
|
|||
external_url: Optional[str]
|
||||
extra_data: Optional[dict[str, str | int | float]]
|
||||
|
||||
|
||||
class DataOverlayFeatureGeometrySchema(WithGeometrySchema, DjangoModelSchema):
|
||||
"""
|
||||
A feature (any kind of geometry) to be displayed as part of a data overlay.
|
||||
"""
|
||||
geometry: AnyGeometrySchema
|
||||
|
||||
class DataOverlayFeatureUpdateSchema(BaseSchema):
|
||||
"""
|
||||
An update to a data overlay feature.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue