diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index 3b52a5e7..1cd674a2 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -64,7 +64,7 @@ class LevelGeometryMixin(GeometryMixin): _('Level'), { 'id': self.level_id, - 'slug': self.level.get_slug(), + 'slug': self.level.effective_slug, 'title': self.level.title, 'can_search': self.level.can_search, }, diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index e2747cbb..7f9ed6ae 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -93,7 +93,7 @@ class SpaceGeometryMixin(GeometryMixin): _('Space'), { 'id': self.space_id, - 'slug': self.space.get_slug(), + 'slug': self.space.effective_slug, 'title': self.space.title, 'can_search': self.space.can_search, }, diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 5c133d87..dd20a88c 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -81,18 +81,20 @@ class LocationSlug(SerializableMixin, models.Model): return getattr(self, model._meta.default_related_name) return None - def get_slug(self): + @property + def effective_slug(self): return self.slug def _serialize(self, **kwargs): result = super()._serialize(**kwargs) result["locationtype"] = self.__class__.__name__.lower() - result['slug'] = self.get_slug() + result['slug'] = self.slug + result['effective_slug'] = self.effective_slug return result def details_display(self, **kwargs): result = super().details_display(**kwargs) - result['display'].insert(2, (_('Slug'), self.get_slug())) + result['display'].insert(2, (_('Slug'), self.effective_slug)) return result @cached_property @@ -116,9 +118,9 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): def serialize(self, detailed=True, **kwargs): result = super().serialize(detailed=detailed, **kwargs) if not detailed: - fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square', - 'locations', 'on_top_of', 'effective_label_settings', 'label_override', 'add_search', 'dynamic', - 'locationtype', 'geometry') + fields = ('id', 'type', 'slug', 'effective_slug', 'title', 'subtitle', 'icon', 'point', 'bounds', + 'grid_square', 'locations', 'on_top_of', 'effective_label_settings', 'label_override', + 'add_search', 'dynamic', 'locationtype', 'geometry') result = {name: result[name] for name in fields if name in result} return result @@ -144,7 +146,8 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): ]) return result - def get_slug(self): + @property + def effective_slug(self): if self.slug is None: code = self.LOCATION_TYPE_CODES.get(self.__class__.__name__) if code is not None: @@ -244,7 +247,7 @@ class SpecificLocation(Location, models.Model): category.title if category.single else category.title_plural, tuple({ 'id': group.pk, - 'slug': group.get_slug(), + 'slug': group.effective_slug, 'title': group.title, 'can_search': group.can_search, } for group in sorted(groups, key=attrgetter('priority'), reverse=True)) @@ -356,6 +359,8 @@ class LocationGroupManager(models.Manager): class LocationGroup(Location, models.Model): + new_serialize = True + class CanReportMissing(models.TextChoices): DONT_OFFER = "dont_offer", _("don't offer") REJECT = "reject", _("offer in first step, then reject") @@ -692,7 +697,8 @@ class Position(CustomLocationProxyMixin, models.Model): def slug(self): return 'p:%s' % self.secret - def get_slug(self): + @property + def effective_slug(self): return self.slug def serialize(self, *args, **kwargs): diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index 568d4b6b..f01976bc 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -41,11 +41,15 @@ class DjangoModelSchema(BaseSchema): class LocationSlugSchema(BaseSchema): - slug: NonEmptyStr = APIField( + slug: Optional[NonEmptyStr] = APIField( title="location slug", description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs" - "are meants to be human-readable and easy to remember.\n\n" - "if a location doesn't have a slug defined, this field holds a slug generated from the " + "are meants to be human-readable and easy to remember.", + example="entrance", + ) + effective_slug: NonEmptyStr = APIField( + title="effective location slug", + description="if a location doesn't have a slug defined, this field holds a slug generated from the " "location type and ID, which will work even if a human-readable slug is defined later.\n\n" "even dynamic locations like coordinates have an (auto-generated) slug.", example="entrance", diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 3a19bbb9..13b83cb4 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -222,16 +222,7 @@ class LocationGroupSchema(LocationSchema, DjangoModelSchema): ) priority: int = APIField() # todo: ??? hierarchy: int = APIField() # todo: ??? - label_settings: Union[ - Annotated[LabelSettingsSchema, APIField( - title="label settings", - description="label settings to use for gruop members that don't have their own set", - )], - Annotated[None, APIField( - title="null", - description="no label settings set" - )], - ] = APIField( + label_settings: Optional[PositiveInt] = APIField( default=None, title="label settings", description=( diff --git a/src/c3nav/mapdata/utils/cache/stats.py b/src/c3nav/mapdata/utils/cache/stats.py index 6bf54c1f..f718639c 100644 --- a/src/c3nav/mapdata/utils/cache/stats.py +++ b/src/c3nav/mapdata/utils/cache/stats.py @@ -90,7 +90,7 @@ def convert_locate(data): for measurement in BeaconMeasurement.objects.all().select_related('space', 'space__level'): pos = CustomLocation(measurement.space.level, measurement.geometry.x, measurement.geometry.y, permissions=set()) - space_slug = measurement.space.get_slug() + space_slug = measurement.space.effective_slug level_label = measurement.space.level.short_label grid_square = pos.grid_square if grid.enabled else None measurement_lookup[pos.pk] = (measurement.pk, grid_square, space_slug, level_label) @@ -143,23 +143,23 @@ def convert_location(data): location = location.get_child() if isinstance(location, LocationRedirect): continue - result['locations']['by_type'].setdefault(location.__class__.__name__.lower(), {})[location.get_slug()] = 0 - location_slugs[location.pk] = location.get_slug() + result['locations']['by_type'].setdefault(location.__class__.__name__.lower(), {})[location.effective_slug] = 0 + location_slugs[location.pk] = location.effective_slug if isinstance(location, Level): result['locations']['by_level'][location.short_label] = 0 result['coordinates']['by_level'][location.short_label] = 0 level_labels[location.pk] = location.short_label if isinstance(location, Space): - result['locations']['by_space'][location.get_slug()] = 0 - result['coordinates']['by_space'][location.get_slug()] = 0 + result['locations']['by_space'][location.effective_slug] = 0 + result['coordinates']['by_space'][location.effective_slug] = 0 if isinstance(location, Area): if getattr(location, 'can_search', False) or getattr(location, 'can_describe', False): - result['coordinates']['by_area'][location.get_slug()] = 0 + result['coordinates']['by_area'][location.effective_slug] = 0 if isinstance(location, POI): if getattr(location, 'can_search', False) or getattr(location, 'can_describe', False): - result['coordinates']['by_poi'][location.get_slug()] = 0 + result['coordinates']['by_poi'][location.effective_slug] = 0 if isinstance(location, LocationGroup): - result['locations']['by_group'][location.get_slug()] = 0 + result['locations']['by_group'][location.effective_slug] = 0 for name, value in data: if name[0] != 'pk' or name[0] == 'c:anywhere': @@ -187,7 +187,7 @@ def convert_location(data): result['locations']['total'] += value location = getattr(location, 'target', location) result['locations']['by_type'].setdefault(location.__class__.__name__.lower(), - {})[location.get_slug()] += value + {})[location.effective_slug] += value if hasattr(location, 'space_id'): result['locations']['by_space'][location_slugs[location.space_id]] += value if hasattr(location, 'level_id'): diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 2e3a3ba0..451f313a 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -342,32 +342,32 @@ class CustomLocation: (_('Slug'), self.pk), (_('Level'), { 'id': self.level.pk, - 'slug': self.level.get_slug(), + 'slug': self.level.effective_slug, 'title': self.level.title, 'can_search': self.level.can_search, }), (_('Space'), { 'id': self.space.pk, - 'slug': self.space.get_slug(), + 'slug': self.space.effective_slug, 'title': self.space.title, 'can_search': self.space.can_search, } if self.space else None), (_('Areas'), tuple({ 'id': area.pk, - 'slug': area.get_slug(), + 'slug': area.effective_slug, 'title': area.title, 'can_search': area.can_search, } for area in self.areas)), (_('Grid Square'), self.grid_square or None), (_('Near Area'), { 'id': self.near_area.pk, - 'slug': self.near_area.get_slug(), + 'slug': self.near_area.effective_slug, 'title': self.near_area.title, 'can_search': self.near_area.can_search, } if self.near_area else None), (_('Near POI'), { 'id': self.near_poi.pk, - 'slug': self.near_poi.get_slug(), + 'slug': self.near_poi.effective_slug, 'title': self.near_poi.title, 'can_search': self.near_poi.can_search, } if self.near_poi else None), @@ -459,7 +459,8 @@ class CustomLocation: def get_icon(self): return self.icon - def get_slug(self): + @property + def effective_slug(self): return self.pk @cached_property diff --git a/src/c3nav/mapdata/views.py b/src/c3nav/mapdata/views.py index 389dcdca..2fe06914 100644 --- a/src/c3nav/mapdata/views.py +++ b/src/c3nav/mapdata/views.py @@ -150,7 +150,7 @@ def preview_location(request, slug): if location is None: raise Http404 - slug = location.get_slug() + slug = location.effective_slug if isinstance(location, CustomLocation): geometries = [Point(location.x, location.y)] diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 713eb7f5..d59fc445 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -169,7 +169,7 @@ c3nav = { location.elem = c3nav._build_location_html(location); location.title_words = location.title.toLowerCase().split(/\s+/); location.subtitle_words = location.subtitle.toLowerCase().split(/\s+/); - location.match = ' ' + location.title_words.join(' ') + ' ' + location.subtitle_words.join(' ') + ' ' + location.slug + ' ' + location.add_search.toLowerCase(); + location.match = ' ' + location.title_words.join(' ') + ' ' + location.subtitle_words.join(' ') + ' ' + location.effective_slug + ' ' + location.add_search.toLowerCase(); locations.push(location); locations_by_id[location.id] = location; if (location.point && location.label_settings) { @@ -464,7 +464,7 @@ c3nav = { for (var j = 0; j < sublocations.length; j++) { loc = sublocations[j]; if (loc.can_search) { - loclist.append($('').attr('href', '/l/' + loc.slug + '/details/').attr('data-id', loc.id).click(function (e) { + loclist.append($('').attr('href', '/l/' + loc.effective_slug + '/details/').attr('data-id', loc.id).click(function (e) { e.preventDefault(); c3nav._locationinput_set($('#destination-input'), c3nav.locations_by_id[parseInt($(this).attr('data-id'))]); c3nav.update_state(false, false, true); @@ -736,12 +736,12 @@ c3nav = { var url = embed ? '/embed' : ''; if (state.routing) { if (state.origin) { - url += (state.destination) ? '/r/' + state.origin.slug + '/' + state.destination.slug + '/' : '/o/' + state.origin.slug + '/'; + url += (state.destination) ? '/r/' + state.origin.effective_slug + '/' + state.destination.effective_slug + '/' : '/o/' + state.origin.effective_slug + '/'; } else { - url += (state.destination) ? '/d/' + state.destination.slug + '/' : '/r/'; + url += (state.destination) ? '/d/' + state.destination.effective_slug + '/' : '/r/'; } } else { - url += state.destination ? ('/l/' + state.destination.slug + '/') : '/'; + url += state.destination ? ('/l/' + state.destination.effective_slug + '/') : '/'; } if (state.details && (url.startsWith('/l/') || url.startsWith('/r/'))) { url += 'details/' @@ -900,9 +900,8 @@ c3nav = { _buttons_share_click: function (location) { var url = c3nav._get_share_url(false, location); if (navigator.share) { - var title - , subtitle; - if (location.slug) { + var title, subtitle; + if (location.effective_slug) { title = location.title; subtitle = location.subtitle; } else { @@ -923,8 +922,8 @@ c3nav = { }, _get_share_url: function (with_position, location) { var url, state = $.extend({}, c3nav.state); - if (location.slug) { - url = '/l/' + location.slug + '/'; + if (location.effective_slug) { + url = '/l/' + location.effective_slug + '/'; } else { if (!with_position) { state.center = null; diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index fa414fd4..e715e11d 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -150,24 +150,24 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N metadata = { 'title': _('Route from %s to %s') % (origin.title, destination.title), 'preview_img_url': request.build_absolute_uri(reverse('mapdata.preview.route', kwargs={ - 'slug': origin.get_slug(), - 'slug2': destination.get_slug(), + 'slug': origin.effective_slug, + 'slug2': destination.effective_slug, })), 'canonical_url': request.build_absolute_uri(reverse('site.index', kwargs={ 'mode': 'r', - 'slug': origin.get_slug(), - 'slug2': destination.get_slug(), + 'slug': origin.effective_slug, + 'slug2': destination.effective_slug, 'details': False, 'options': False, })), } elif destination is not None or origin is not None: if destination is not None: - loc_slug = destination.get_slug() + loc_slug = destination.effective_slug title = destination.title subtitle = destination.subtitle if hasattr(destination, 'subtitle') else None else: - loc_slug = origin.get_slug() + loc_slug = origin.effective_slug title = origin.title subtitle = origin.subtitle if hasattr(origin, 'subtitle') else None metadata = { @@ -579,7 +579,7 @@ def report_missing_choose(request, coordinates): 'locations': [ { "url": reverse('site.report_create', - kwargs={"coordinates": coordinates, "group": group.get_slug()}), + kwargs={"coordinates": coordinates, "group": group.effective_slug}), "location": group, "replace_subtitle": group.description }