implement new serialize for level
This commit is contained in:
parent
899a9e01ee
commit
35a8738424
5 changed files with 98 additions and 49 deletions
|
@ -1,6 +1,10 @@
|
||||||
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
from types import NoneType
|
from types import NoneType
|
||||||
from typing import Annotated, Any, Literal, Union
|
from typing import Annotated, Any, Literal, Union, ClassVar
|
||||||
|
|
||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
|
from django.db.models import Model, ManyToManyField
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import Discriminator
|
from pydantic import Discriminator
|
||||||
|
@ -12,31 +16,62 @@ from pydantic_core.core_schema import ValidationInfo
|
||||||
from c3nav.api.utils import NonEmptyStr
|
from c3nav.api.utils import NonEmptyStr
|
||||||
|
|
||||||
|
|
||||||
|
def make_serializable(values: Any):
|
||||||
|
if isinstance(values, Schema):
|
||||||
|
return values
|
||||||
|
if isinstance(values, (str, bool, int, float, complex, NoneType)):
|
||||||
|
return values
|
||||||
|
if isinstance(values, dict):
|
||||||
|
return {
|
||||||
|
key: make_serializable(val)
|
||||||
|
for key, val in values.items()
|
||||||
|
}
|
||||||
|
if isinstance(values, (list, tuple, set, frozenset)):
|
||||||
|
return type(values)(make_serializable(val) for val in values)
|
||||||
|
if isinstance(values, Promise):
|
||||||
|
return str(values)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModelDataForwarder:
|
||||||
|
obj: Model
|
||||||
|
overrides: dict
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
with suppress(KeyError):
|
||||||
|
return make_serializable(self.overrides[key])
|
||||||
|
with suppress(FieldDoesNotExist):
|
||||||
|
field = self.obj._meta.get_field(key)
|
||||||
|
if field.is_relation:
|
||||||
|
if field.many_to_many:
|
||||||
|
return [obj.pk for obj in getattr(self.obj, key).all()]
|
||||||
|
return make_serializable(getattr(self.obj, field.attname))
|
||||||
|
return make_serializable(getattr(self.obj, key))
|
||||||
|
|
||||||
|
|
||||||
class BaseSchema(Schema):
|
class BaseSchema(Schema):
|
||||||
|
orig_keys: ClassVar[frozenset[str]] = frozenset()
|
||||||
|
|
||||||
@model_validator(mode="wrap") # noqa
|
@model_validator(mode="wrap") # noqa
|
||||||
@classmethod
|
@classmethod
|
||||||
def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any:
|
def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any:
|
||||||
""" overwriting this, we need to call serialize to get the correct data """
|
""" overwriting this, we need to call serialize to get the correct data """
|
||||||
return handler(cls.convert(values))
|
if hasattr(values, 'serialize') and callable(values.serialize) and not getattr(values, 'new_serialize', False):
|
||||||
|
converted = make_serializable(values.serialize())
|
||||||
|
elif isinstance(values, Model):
|
||||||
|
converted = ModelDataForwarder(
|
||||||
|
obj=values,
|
||||||
|
overrides=cls.get_overrides(values),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
converted = make_serializable(values)
|
||||||
|
return handler(converted)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert(cls, values: Any):
|
def get_overrides(cls, value: Model) -> dict:
|
||||||
if isinstance(values, Schema):
|
return {}
|
||||||
return values
|
|
||||||
if isinstance(values, (str, bool, int, float, complex, NoneType)):
|
|
||||||
return values
|
|
||||||
if isinstance(values, dict):
|
|
||||||
return {
|
|
||||||
key: cls.convert(val)
|
|
||||||
for key, val in values.items()
|
|
||||||
}
|
|
||||||
if isinstance(values, (list, tuple, set, frozenset)):
|
|
||||||
return type(values)(cls.convert(val) for val in values)
|
|
||||||
if isinstance(values, Promise):
|
|
||||||
return str(values)
|
|
||||||
if hasattr(values, 'serialize') and callable(values.serialize):
|
|
||||||
return cls.convert(values.serialize())
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
class APIErrorSchema(BaseSchema):
|
class APIErrorSchema(BaseSchema):
|
||||||
|
|
|
@ -14,8 +14,12 @@ from c3nav.mapdata.models.locations import SpecificLocation
|
||||||
|
|
||||||
class Level(SpecificLocation, models.Model):
|
class Level(SpecificLocation, models.Model):
|
||||||
"""
|
"""
|
||||||
A map level
|
A physical level of the map, containing building, spaces, doors…
|
||||||
|
|
||||||
|
A level is a specific location, and can therefore be routed to and from, as well as belong to location groups.
|
||||||
"""
|
"""
|
||||||
|
new_serialize = True
|
||||||
|
|
||||||
base_altitude = models.DecimalField(_('base altitude'), null=False, unique=True, max_digits=6, decimal_places=2)
|
base_altitude = models.DecimalField(_('base altitude'), null=False, unique=True, max_digits=6, decimal_places=2)
|
||||||
default_height = models.DecimalField(_('default space height'), max_digits=6, decimal_places=2, default=3.0,
|
default_height = models.DecimalField(_('default space height'), max_digits=6, decimal_places=2, default=3.0,
|
||||||
validators=[MinValueValidator(Decimal('0'))])
|
validators=[MinValueValidator(Decimal('0'))])
|
||||||
|
@ -68,15 +72,6 @@ class Level(SpecificLocation, models.Model):
|
||||||
def primary_level_pk(self):
|
def primary_level_pk(self):
|
||||||
return self.pk if self.on_top_of_id is None else self.on_top_of_id
|
return self.pk if self.on_top_of_id is None else self.on_top_of_id
|
||||||
|
|
||||||
def _serialize(self, level=True, **kwargs):
|
|
||||||
result = super()._serialize(**kwargs)
|
|
||||||
result['short_label'] = self.short_label
|
|
||||||
result['on_top_of'] = self.on_top_of_id
|
|
||||||
result['base_altitude'] = float(str(self.base_altitude))
|
|
||||||
result['default_height'] = float(str(self.default_height))
|
|
||||||
result['door_height'] = float(str(self.door_height))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def details_display(self, editor_url=True, **kwargs):
|
def details_display(self, editor_url=True, **kwargs):
|
||||||
result = super().details_display(**kwargs)
|
result = super().details_display(**kwargs)
|
||||||
result['display'].insert(3, (_('short label'), self.short_label))
|
result['display'].insert(3, (_('short label'), self.short_label))
|
||||||
|
|
|
@ -113,7 +113,7 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def serialize(self, detailed=True, describe_only=False, **kwargs):
|
def serialize(self, detailed=True, **kwargs):
|
||||||
result = super().serialize(detailed=detailed, **kwargs)
|
result = super().serialize(detailed=detailed, **kwargs)
|
||||||
if not detailed:
|
if not detailed:
|
||||||
fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square',
|
fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square',
|
||||||
|
@ -195,23 +195,19 @@ class SpecificLocation(Location, models.Model):
|
||||||
if grid_square is not None:
|
if grid_square is not None:
|
||||||
result['grid_square'] = grid_square or None
|
result['grid_square'] = grid_square or None
|
||||||
if detailed:
|
if detailed:
|
||||||
groups = {}
|
result['groups'] = self.groups_by_category
|
||||||
for group in self.groups.all():
|
|
||||||
groups.setdefault(group.category, []).append(group.pk)
|
|
||||||
groups = {category.name: (items[0] if items else None) if category.single else items
|
|
||||||
for category, items in groups.items()
|
|
||||||
if getattr(category, 'allow_'+self.__class__._meta.default_related_name)}
|
|
||||||
result['groups'] = groups
|
|
||||||
|
|
||||||
label_settings = self.get_label_settings()
|
result["label_settings"] = self.label_settings_id
|
||||||
if label_settings:
|
effective_label_settings = self.effective_label_settings
|
||||||
result['label_settings'] = label_settings.serialize(detailed=False)
|
if effective_label_settings:
|
||||||
|
result['effective_label_settings'] = effective_label_settings.serialize(detailed=False)
|
||||||
if self.label_overrides:
|
if self.label_overrides:
|
||||||
# todo: what if only one language is set?
|
# todo: what if only one language is set?
|
||||||
result['label_override'] = self.label_override
|
result['label_override'] = self.label_override
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_label_settings(self):
|
@property
|
||||||
|
def effective_label_settings(self):
|
||||||
if self.label_settings:
|
if self.label_settings:
|
||||||
return self.label_settings
|
return self.label_settings
|
||||||
for group in self.groups.all():
|
for group in self.groups.all():
|
||||||
|
@ -219,6 +215,16 @@ class SpecificLocation(Location, models.Model):
|
||||||
return group.label_settings
|
return group.label_settings
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups_by_category(self):
|
||||||
|
groups_by_category = {}
|
||||||
|
for group in self.groups.all():
|
||||||
|
groups_by_category.setdefault(group.category, []).append(group.pk)
|
||||||
|
groups_by_category = {category.name: (items[0] if items else None) if category.single else items
|
||||||
|
for category, items in groups_by_category.items()
|
||||||
|
if getattr(category, 'allow_' + self.__class__._meta.default_related_name)}
|
||||||
|
return groups_by_category
|
||||||
|
|
||||||
def details_display(self, **kwargs):
|
def details_display(self, **kwargs):
|
||||||
result = super().details_display(**kwargs)
|
result = super().details_display(**kwargs)
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,12 @@ class SpecificLocationSchema(LocationSchema):
|
||||||
description="grid cell(s) that this location is in, if a grid is defined and the location is within it",
|
description="grid cell(s) that this location is in, if a grid is defined and the location is within it",
|
||||||
example="C3",
|
example="C3",
|
||||||
)
|
)
|
||||||
groups: dict[
|
groups: list[PositiveInt] = APIField(
|
||||||
|
title="location groups",
|
||||||
|
description="location group(s) that this specific location belongs to.",
|
||||||
|
example=[5, 1, 3, 7],
|
||||||
|
)
|
||||||
|
groups_by_category: dict[
|
||||||
Annotated[NonEmptyStr, APIField(title="location group category name")],
|
Annotated[NonEmptyStr, APIField(title="location group category name")],
|
||||||
Union[
|
Union[
|
||||||
Annotated[list[PositiveInt], APIField(
|
Annotated[list[PositiveInt], APIField(
|
||||||
|
@ -163,7 +168,7 @@ class SpecificLocationSchema(LocationSchema):
|
||||||
)],
|
)],
|
||||||
]
|
]
|
||||||
] = APIField(
|
] = APIField(
|
||||||
title="location groups",
|
title="location groups by category",
|
||||||
description="location group(s) that this specific location belongs to, grouped by categories.\n\n"
|
description="location group(s) that this specific location belongs to, grouped by categories.\n\n"
|
||||||
"keys are location group category names. see location group category endpoint for details.\n\n"
|
"keys are location group category names. see location group category endpoint for details.\n\n"
|
||||||
"categories may be missing if no groups apply.",
|
"categories may be missing if no groups apply.",
|
||||||
|
@ -173,7 +178,15 @@ class SpecificLocationSchema(LocationSchema):
|
||||||
"category_with_single_false": [1, 3, 7],
|
"category_with_single_false": [1, 3, 7],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
label_settings: Union[
|
label_settings: Optional[PositiveInt] = APIField(
|
||||||
|
default=None,
|
||||||
|
title="label settings",
|
||||||
|
description=(
|
||||||
|
schema_description(LabelSettingsSchema) +
|
||||||
|
"\n\nif not set, label settings of location groups might be used"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
effective_label_settings: Union[
|
||||||
Annotated[LabelSettingsSchema, APIField(
|
Annotated[LabelSettingsSchema, APIField(
|
||||||
title="label settings",
|
title="label settings",
|
||||||
description="label settings to use",
|
description="label settings to use",
|
||||||
|
|
|
@ -152,8 +152,8 @@ c3nav = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_sort_labels: function (a, b) {
|
_sort_labels: function (a, b) {
|
||||||
var result = (a[0].label_settings.min_zoom || -10) - (b[0].label_settings.min_zoom || -10);
|
var result = (a[0].effective_label_settings.min_zoom || -10) - (b[0].effective_label_settings.min_zoom || -10);
|
||||||
if (result === 0) result = b[0].label_settings.font_size - a[0].label_settings.font_size;
|
if (result === 0) result = b[0].effective_label_settings.font_size - a[0].effective_label_settings.font_size;
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
_last_time_searchable_locations_loaded: null,
|
_last_time_searchable_locations_loaded: null,
|
||||||
|
@ -406,12 +406,12 @@ c3nav = {
|
||||||
for (var item of labels) {
|
for (var item of labels) {
|
||||||
location = item[0];
|
location = item[0];
|
||||||
label = item[1];
|
label = item[1];
|
||||||
if (zoom < (location.label_settings.min_zoom || -10)) {
|
if (zoom < (location.effective_label_settings.min_zoom || -10)) {
|
||||||
// since the labels are sorted by min_zoom, we can just leave here
|
// since the labels are sorted by min_zoom, we can just leave here
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (bounds.contains(label.getLatLng())) {
|
if (bounds.contains(label.getLatLng())) {
|
||||||
if ((location.label_settings.max_zoom || 10) > zoom) {
|
if ((location.effective_label_settings.max_zoom || 10) > zoom) {
|
||||||
c3nav._labelLayer._maybeAddLayerToRBush(label);
|
c3nav._labelLayer._maybeAddLayerToRBush(label);
|
||||||
} else {
|
} else {
|
||||||
valid_upper.unshift(label);
|
valid_upper.unshift(label);
|
||||||
|
@ -1008,7 +1008,7 @@ c3nav = {
|
||||||
new_text[i] = new_text[i].trim();
|
new_text[i] = new_text[i].trim();
|
||||||
}
|
}
|
||||||
var html = $('<div class="location-label-text">').append($('<span>').html(' ' + new_text.join(' <br> ') + ' '));
|
var html = $('<div class="location-label-text">').append($('<span>').html(' ' + new_text.join(' <br> ') + ' '));
|
||||||
html.css('font-size', location.label_settings.font_size + 'px');
|
html.css('font-size', location.effective_label_settings.font_size + 'px');
|
||||||
return L.marker(L.GeoJSON.coordsToLatLng(location.point.slice(1)), {
|
return L.marker(L.GeoJSON.coordsToLatLng(location.point.slice(1)), {
|
||||||
icon: L.divIcon({
|
icon: L.divIcon({
|
||||||
html: html[0].outerHTML,
|
html: html[0].outerHTML,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue