team-3/src/c3nav/mapdata/models/locations.py

651 lines
25 KiB
Python
Raw Normal View History

2019-12-27 14:13:40 +01:00
import string
2024-02-07 18:34:28 +01:00
import typing
from contextlib import suppress
2019-12-27 18:31:54 +01:00
from datetime import timedelta
2019-12-22 00:38:54 +01:00
from decimal import Decimal
2017-11-02 13:35:58 +01:00
from operator import attrgetter
2019-12-27 14:13:40 +01:00
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist
2019-12-22 00:45:11 +01:00
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models, transaction
from django.db.models import Prefetch
2017-11-02 13:35:58 +01:00
from django.urls import reverse
2019-12-27 18:31:54 +01:00
from django.utils import timezone
2019-12-27 14:13:40 +01:00
from django.utils.crypto import get_random_string
2017-10-28 13:31:12 +02:00
from django.utils.functional import cached_property
from django.utils.text import format_lazy
2022-04-03 16:42:17 +02:00
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
2017-11-30 16:51:28 +01:00
from c3nav.mapdata.fields import I18nField
from c3nav.mapdata.grid import grid
from c3nav.mapdata.models.access import AccessRestrictionMixin
2017-07-10 13:01:35 +02:00
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
2019-12-27 14:13:40 +01:00
from c3nav.mapdata.utils.fields import LocationById
2017-06-22 19:27:51 +02:00
from c3nav.mapdata.utils.models import get_submodels
2017-05-11 19:36:49 +02:00
2024-02-07 18:34:28 +01:00
if typing.TYPE_CHECKING:
from c3nav.mapdata.render.theme import ThemeColorManager
2017-06-18 18:42:30 +02:00
class LocationSlugManager(models.Manager):
def get_queryset(self):
result = super().get_queryset()
if self.model == LocationSlug:
for model in get_submodels(Location) + [LocationRedirect]:
result = result.select_related(model._meta.default_related_name)
try:
model._meta.get_field('space')
except FieldDoesNotExist:
pass
else:
result = result.select_related(model._meta.default_related_name+'__space')
return result
2017-06-18 18:42:30 +02:00
def select_related_target(self):
if self.model != LocationSlug:
raise TypeError
qs = self.get_queryset()
qs = qs.select_related('locationredirects__target',
*('locationredirects__target__'+model._meta.default_related_name
for model in get_submodels(Location) + [LocationRedirect]))
return qs
2017-06-18 18:42:30 +02:00
2018-12-23 18:13:37 +01:00
validate_slug = RegexValidator(
2018-12-24 16:09:34 +01:00
r'^[a-z0-9]+(--?[a-z0-9]+)*\Z',
2018-12-23 18:13:37 +01:00
# Translators: "letters" means latin letters: a-z and A-Z.
_('Enter a valid location slug consisting of lowercase letters, numbers or hyphens, '
'not starting or ending with hyphens or containing consecutive hyphens.'),
'invalid'
)
class LocationSlug(SerializableMixin, models.Model):
LOCATION_TYPE_CODES = {
2017-06-11 14:43:14 +02:00
'Level': 'l',
'Space': 's',
'Area': 'a',
2017-07-08 16:29:12 +02:00
'POI': 'p',
'LocationGroup': 'g'
}
LOCATION_TYPE_BY_CODE = {code: model_name for model_name, code in LOCATION_TYPE_CODES.items()}
2018-12-23 18:13:37 +01:00
slug = models.SlugField(_('Slug'), unique=True, null=True, blank=True, max_length=50, validators=[validate_slug])
2017-06-18 16:52:50 +02:00
2017-06-18 18:42:30 +02:00
objects = LocationSlugManager()
2017-05-10 18:03:57 +02:00
def get_child(self):
2017-06-22 19:27:51 +02:00
for model in get_submodels(Location)+[LocationRedirect]:
with suppress(AttributeError):
return getattr(self, model._meta.default_related_name)
2017-05-11 19:36:49 +02:00
return None
2024-12-03 16:07:07 +01:00
@property
def effective_slug(self):
2017-05-12 12:54:10 +02:00
return self.slug
2018-09-19 19:08:47 +02:00
def details_display(self, **kwargs):
result = super().details_display(**kwargs)
2024-12-03 16:07:07 +01:00
result['display'].insert(2, (_('Slug'), self.effective_slug))
2017-11-02 13:35:58 +01:00
return result
2017-10-28 13:31:12 +02:00
@cached_property
def order(self):
return (-1, 0)
2017-05-10 18:03:57 +02:00
class Meta:
verbose_name = _('Location with Slug')
2017-07-10 13:04:02 +02:00
verbose_name_plural = _('Location with Slug')
2017-05-10 18:03:57 +02:00
default_related_name = 'locationslugs'
class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model):
2017-05-10 18:03:57 +02:00
can_search = models.BooleanField(default=True, verbose_name=_('can be searched'))
2017-11-01 15:55:59 +01:00
can_describe = models.BooleanField(default=True, verbose_name=_('can describe'))
2018-12-21 03:07:07 +01:00
icon = models.CharField(_('icon'), max_length=32, null=True, blank=True, help_text=_('any material icons name'))
2017-05-10 18:03:57 +02:00
class Meta:
abstract = True
2024-12-03 19:00:35 +01:00
@property
def add_search(self):
return ' '.join((
*(redirect.slug for redirect in self.redirects.all()),
*self.other_titles,
))
2017-05-10 18:03:57 +02:00
2018-09-19 19:08:47 +02:00
def details_display(self, **kwargs):
result = super().details_display(**kwargs)
2017-11-02 13:35:58 +01:00
result['display'].extend([
(_('searchable'), _('Yes') if self.can_search else _('No')),
2018-12-21 03:07:07 +01:00
(_('can describe'), _('Yes') if self.can_describe else _('No')),
2024-12-03 19:00:35 +01:00
(_('icon'), self.effective_icon),
2017-11-02 13:35:58 +01:00
])
return result
2024-12-03 16:07:07 +01:00
@property
def effective_slug(self):
if self.slug is None:
code = self.LOCATION_TYPE_CODES.get(self.__class__.__name__)
if code is not None:
return code+':'+str(self.id)
return self.slug
2017-10-27 13:47:12 +02:00
@property
def subtitle(self):
return ''
2018-12-10 19:06:38 +01:00
@property
def grid_square(self):
2018-12-10 19:06:38 +01:00
return None
def get_color(self, color_manager: 'ThemeColorManager') -> str | None:
# don't filter in the query here so prefetch_related works
result = self.get_color_sorted(color_manager)
2018-12-21 19:06:29 +01:00
return None if result is None else result[1]
def get_color_sorted(self, color_manager: 'ThemeColorManager') -> tuple[tuple, str] | None:
# don't filter in the query here so prefetch_related works
for group in self.groups.all():
2024-01-06 13:26:34 +01:00
color = color_manager.locationgroup_fill_color(group)
if color and getattr(group.category, 'allow_'+self.__class__._meta.default_related_name):
return (0, group.category.priority, group.hierarchy, group.priority), color
return None
2017-05-13 16:39:01 +02:00
2024-12-03 19:00:35 +01:00
@property
def effective_icon(self):
2018-12-21 03:07:07 +01:00
return self.icon or None
2017-05-10 18:03:57 +02:00
class SpecificLocation(Location, models.Model):
groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('Location Groups'), blank=True)
2019-12-22 02:06:23 +01:00
label_settings = models.ForeignKey('mapdata.LabelSettings', null=True, blank=True, on_delete=models.PROTECT,
2019-12-22 00:38:54 +01:00
verbose_name=_('label settings'))
label_override = I18nField(_('Label override'), plural_name='label_overrides', blank=True, fallback_any=True)
2023-12-22 01:19:53 +01:00
external_url = models.URLField(_('external URL'), null=True, blank=True)
2023-12-25 16:48:39 +01:00
import_block_data = models.BooleanField(_('don\'t change metadata on import'), default=False)
import_block_geom = models.BooleanField(_('don\'t change geometry on import'), default=False)
2017-05-10 18:03:57 +02:00
class Meta:
abstract = True
2024-12-03 14:18:16 +01:00
@property
def effective_label_settings(self):
2019-12-22 00:38:54 +01:00
if self.label_settings:
return self.label_settings
for group in self.groups.all():
if group.label_settings:
return group.label_settings
return None
2019-12-21 23:01:24 +01:00
2024-12-03 14:18:16 +01:00
@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
2018-09-19 19:08:47 +02:00
def details_display(self, **kwargs):
result = super().details_display(**kwargs)
2017-11-02 13:35:58 +01:00
groupcategories = {}
for group in self.groups.all():
groupcategories.setdefault(group.category, []).append(group)
if grid.enabled:
grid_square = self.grid_square
if grid_square is not None:
grid_square_title = (_('Grid Squares') if grid_square and '-' in grid_square else _('Grid Square'))
result['display'].insert(3, (grid_square_title, grid_square or None))
2018-12-10 19:32:28 +01:00
2017-11-02 13:35:58 +01:00
for category, groups in sorted(groupcategories.items(), key=lambda item: item[0].priority):
result['display'].insert(3, (
category.title if category.single else category.title_plural,
tuple({
'id': group.pk,
2024-12-03 16:07:07 +01:00
'slug': group.effective_slug,
'title': group.title,
'can_search': group.can_search,
} for group in sorted(groups, key=attrgetter('priority'), reverse=True))
))
2017-11-02 13:35:58 +01:00
2023-12-22 01:19:53 +01:00
if self.external_url:
2023-12-26 00:12:06 +01:00
result['display'].insert(3, (_('External URL'), {
2023-12-22 01:19:53 +01:00
'title': _('Open'),
'url': self.external_url,
2023-12-26 00:12:06 +01:00
}))
2023-12-22 01:19:53 +01:00
2017-11-02 13:35:58 +01:00
return result
@cached_property
def describing_groups(self):
2017-12-14 21:44:14 +01:00
groups = tuple(self.groups.all() if 'groups' in getattr(self, '_prefetched_objects_cache', ()) else ())
2017-12-19 19:30:51 +01:00
groups = tuple(group for group in groups if group.can_describe)
return groups
@property
def subtitle(self):
subtitle = self.describing_groups[0].title if self.describing_groups else self.__class__._meta.verbose_name
if self.grid_square:
return '%s, %s' % (subtitle, self.grid_square)
2018-12-10 19:06:38 +01:00
return subtitle
2017-10-27 13:47:12 +02:00
2017-10-28 13:31:12 +02:00
@cached_property
def order(self):
groups = tuple(self.groups.all())
if not groups:
2017-12-11 01:51:58 +01:00
return (0, 0, 0)
2017-10-28 13:31:12 +02:00
return (0, groups[0].category.priority, groups[0].priority)
2024-12-03 19:00:35 +01:00
@property
def effective_icon(self):
icon = super().effective_icon
2018-12-21 03:24:58 +01:00
if icon:
return icon
for group in self.groups.all():
if group.icon and getattr(group.category, 'allow_' + self.__class__._meta.default_related_name):
return group.icon
return None
2018-12-21 03:07:07 +01:00
2017-11-30 16:51:28 +01:00
class LocationGroupCategory(SerializableMixin, models.Model):
2017-07-10 13:54:33 +02:00
name = models.SlugField(_('Name'), unique=True, max_length=50)
single = models.BooleanField(_('single selection'), default=False)
2017-11-30 16:51:28 +01:00
title = I18nField(_('Title'), plural_name='titles', fallback_any=True)
title_plural = I18nField(_('Title (Plural)'), plural_name='titles_plural', fallback_any=True)
2018-12-23 17:52:15 +01:00
help_text = I18nField(_('Help text'), plural_name='help_texts', fallback_any=True, fallback_value='')
allow_levels = models.BooleanField(_('allow levels'), db_index=True, default=True)
allow_spaces = models.BooleanField(_('allow spaces'), db_index=True, default=True)
allow_areas = models.BooleanField(_('allow areas'), db_index=True, default=True)
allow_pois = models.BooleanField(_('allow pois'), db_index=True, default=True)
2019-12-27 18:42:08 +01:00
allow_dynamic_locations = models.BooleanField(_('allow dynamic locations'), db_index=True, default=True)
priority = models.IntegerField(default=0, db_index=True)
2017-07-10 13:54:33 +02:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._orig = {"priority": self.priority}
2017-07-10 13:54:33 +02:00
class Meta:
verbose_name = _('Location Group Category')
verbose_name_plural = _('Location Group Categories')
default_related_name = 'locationgroupcategories'
ordering = ('-priority', )
2017-07-10 13:54:33 +02:00
def register_changed_geometries(self):
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
query = self.groups.all()
for model in get_submodels(SpecificLocation):
related_name = model._meta.default_related_name
subquery = model.objects.all()
if issubclass(model, SpaceGeometryMixin):
subquery = subquery.select_related('space')
query.prefetch_related(Prefetch('groups__'+related_name, subquery))
for group in query:
group.register_changed_geometries(do_query=False)
def save(self, *args, **kwargs):
if self.pk and any(getattr(self, attname) != value for attname, value in self._orig.items()):
self.register_changed_geometries()
super().save(*args, **kwargs)
2017-07-10 13:54:33 +02:00
class LocationGroupManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('category')
class LocationGroup(Location, models.Model):
class CanReportMissing(models.TextChoices):
DONT_OFFER = "dont_offer", _("don't offer")
REJECT = "reject", _("offer in first step, then reject")
SINGLE = "single", _("offer in first step, exclusive choice")
MULTIPLE = "multiple", _("offer if nothing in the first step matches, multiple choice")
2017-07-10 13:54:33 +02:00
category = models.ForeignKey(LocationGroupCategory, related_name='groups', on_delete=models.PROTECT,
verbose_name=_('Category'))
priority = models.IntegerField(default=0, db_index=True)
2019-12-21 18:09:59 +01:00
hierarchy = models.IntegerField(default=0, db_index=True, verbose_name=_('hierarchy'))
2019-12-22 02:06:23 +01:00
label_settings = models.ForeignKey('mapdata.LabelSettings', null=True, blank=True, on_delete=models.PROTECT,
2019-12-22 00:38:54 +01:00
verbose_name=_('label settings'),
help_text=_('unless location specifies otherwise'))
can_report_missing = models.CharField(_('report missing location'), choices=CanReportMissing.choices,
default=CanReportMissing.DONT_OFFER, max_length=16)
description = I18nField(_('description'), plural_name='descriptions', blank=True, fallback_any=True,
2024-03-24 17:38:24 +01:00
fallback_value="", help_text=_('to aid with selection in the report form'))
report_help_text = I18nField(_('report help text'), plural_name='report_help_texts', blank=True, fallback_any=True,
2024-03-24 17:38:24 +01:00
fallback_value="", help_text=_('to explain the report form or rejection'))
color = models.CharField(null=True, blank=True, max_length=32, verbose_name=_('background color'))
2024-09-06 14:11:41 +02:00
in_legend = models.BooleanField(default=False, verbose_name=_('show in legend (if color set)'))
2023-12-22 01:19:53 +01:00
hub_import_type = models.CharField(max_length=100, verbose_name=_('hub import type'), null=True, blank=True,
unique=True,
help_text=_('assign this group to imported hub locations of this type'))
objects = LocationGroupManager()
class Meta:
verbose_name = _('Location Group')
verbose_name_plural = _('Location Groups')
default_related_name = 'locationgroups'
ordering = ('-category__priority', '-priority')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._orig = {
"priority": self.priority,
"hierarchy": self.hierarchy,
"category": self.category,
"color": self.color,
}
2024-12-03 18:42:33 +01:00
locations = []
2018-09-19 19:08:47 +02:00
def details_display(self, editor_url=True, **kwargs):
result = super().details_display(**kwargs)
result['display'].insert(3, (_('Category'), self.category.title))
2017-11-02 13:35:58 +01:00
result['display'].extend([
(_('color'), self.color),
(_('priority'), str(self.priority)),
2017-11-02 13:35:58 +01:00
])
2018-09-19 19:08:47 +02:00
if editor_url:
result['editor_url'] = reverse('editor.locationgroups.edit', kwargs={'pk': self.pk})
2017-11-02 13:35:58 +01:00
return result
@property
def title_for_forms(self):
attributes = []
if self.can_search:
attributes.append(_('search'))
if self.can_describe:
attributes.append(_('describe'))
if self.color:
attributes.append(_('color'))
if not attributes:
attributes.append(_('internal'))
return self.title + ' ('+', '.join(str(s) for s in attributes)+')'
def register_changed_geometries(self, do_query=True):
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
for model in get_submodels(SpecificLocation):
query = getattr(self, model._meta.default_related_name).all()
if do_query:
if issubclass(model, SpaceGeometryMixin):
query = query.select_related('space')
for obj in query:
obj.register_change(force=True)
2017-10-28 19:49:28 +02:00
@property
def subtitle(self):
result = self.category.title
if hasattr(self, 'locations'):
return format_lazy(_('{category_title}, {num_locations}'),
category_title=result,
num_locations=(ngettext_lazy('%(num)d location', '%(num)d locations', 'num') %
{'num': len(self.locations)}))
return result
2017-10-28 19:49:28 +02:00
2017-10-28 13:31:12 +02:00
@cached_property
def order(self):
2017-10-28 20:06:44 +02:00
return (1, self.category.priority, self.priority)
2017-10-28 13:31:12 +02:00
def save(self, *args, **kwargs):
if self.pk and any(getattr(self, attname) != value for attname, value in self._orig.items()):
self.register_changed_geometries()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
self.register_changed_geometries()
super().delete(*args, **kwargs)
class LocationRedirect(LocationSlug):
2017-07-10 13:54:33 +02:00
target = models.ForeignKey(LocationSlug, related_name='redirects', on_delete=models.CASCADE,
verbose_name=_('target'))
@property
def target_slug(self):
if type(self.target) is LocationSlug:
return self.target.get_child().effective_slug
return self.target.effective_slug
class Meta:
default_related_name = 'locationredirects'
2019-12-22 00:38:54 +01:00
class LabelSettings(SerializableMixin, models.Model):
title = I18nField(_('Title'), plural_name='titles', fallback_any=True)
min_zoom = models.DecimalField(_('min zoom'), max_digits=3, decimal_places=1, default=-10,
validators=[MinValueValidator(Decimal('-10')),
MaxValueValidator(Decimal('10'))])
max_zoom = models.DecimalField(_('max zoom'), max_digits=3, decimal_places=1, default=10,
validators=[MinValueValidator(Decimal('-10')),
MaxValueValidator(Decimal('10'))])
font_size = models.IntegerField(_('font size'), default=12,
validators=[MinValueValidator(12),
MaxValueValidator(30)])
class Meta:
verbose_name = _('Label Settings')
verbose_name_plural = _('Label Settings')
default_related_name = 'labelsettings'
2019-12-22 01:56:11 +01:00
ordering = ('min_zoom', '-font_size')
2019-12-27 14:13:40 +01:00
class CustomLocationProxyMixin:
request = None
def get_custom_location(self, request=None):
raise NotImplementedError
2019-12-27 23:27:50 +01:00
@property
def available(self):
return self.get_custom_location() is not None
@property
def x(self):
return self.get_custom_location().x
@property
def y(self):
return self.get_custom_location().y
@property
def level(self):
return self.get_custom_location().level
def serialize_position(self, request=None):
raise NotImplementedError
class DynamicLocation(CustomLocationProxyMixin, SpecificLocation, models.Model):
2019-12-27 14:13:40 +01:00
position_secret = models.CharField(_('position secret'), max_length=32, null=True, blank=True)
class Meta:
verbose_name = _('Dynamic location')
verbose_name_plural = _('Dynamic locations')
default_related_name = 'dynamiclocations'
2019-12-27 14:13:40 +01:00
2019-12-27 23:45:26 +01:00
def register_change(self, force=False):
pass
def serialize_position(self, request=None):
# todo: make this pretty
custom_location = self.get_custom_location(request=request)
if custom_location is None:
return {
'available': False,
2019-12-27 23:50:05 +01:00
'id': self.pk,
2023-11-24 01:05:38 +01:00
'slug': self.slug,
2024-12-03 19:00:35 +01:00
'icon': self.effective_icon,
2023-11-24 17:25:34 +01:00
'title': str(self.title),
'subtitle': '%s %s, %s' % (_('currently unavailable'), _('(moving)'), self.subtitle)
}
from c3nav.mapdata.schemas.models import CustomLocationSchema
result = CustomLocationSchema.model_validate(custom_location).model_dump()
result.update({
'available': True,
'id': self.pk,
'slug': self.slug,
2024-12-03 19:00:35 +01:00
'icon': self.effective_icon,
2023-11-24 17:24:20 +01:00
'title': str(self.title),
'subtitle': '%s %s%s, %s' % (
_('(moving)'),
('%s, ' % self.subtitle) if self.describing_groups else '',
result['title'],
result['subtitle']
),
})
return result
def get_custom_location(self, request=None):
if not self.position_secret:
return None
try:
return Position.objects.get(secret=self.position_secret).get_custom_location(
2024-09-18 21:00:13 +02:00
request=request if request is not None else self.request
)
except Position.DoesNotExist:
return None
2019-12-27 14:13:40 +01:00
def details_display(self, editor_url=True, **kwargs):
result = super().details_display(**kwargs)
if editor_url:
result['editor_url'] = reverse('editor.dynamic_locations.edit', kwargs={'pk': self.pk})
2019-12-27 14:13:40 +01:00
return result
def get_position_secret():
return get_random_string(32, string.ascii_letters+string.digits)
class Position(CustomLocationProxyMixin, models.Model):
2019-12-27 14:13:40 +01:00
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(_('name'), max_length=32)
secret = models.CharField(_('secret'), unique=True, max_length=32, default=get_position_secret)
2019-12-27 16:51:06 +01:00
last_coordinates_update = models.DateTimeField(_('last coordinates update'), null=True)
2019-12-28 16:35:38 +01:00
timeout = models.PositiveSmallIntegerField(_('timeout (in seconds)'), default=0, blank=True,
help_text=_('0 for no timeout'))
coordinates_id = models.CharField(_('coordinates'), null=True, blank=True, max_length=48)
2019-12-27 14:13:40 +01:00
2019-12-27 23:27:50 +01:00
can_search = True
can_describe = False
2019-12-27 14:13:40 +01:00
coordinates = LocationById()
class Meta:
verbose_name = _('Dynamic position')
verbose_name_plural = _('Dynamic position')
default_related_name = 'dynamic_positions'
2019-12-27 18:31:54 +01:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.timeout and self.last_coordinates_update:
end_time = self.last_coordinates_update + timedelta(seconds=self.timeout)
if timezone.now() >= end_time:
2019-12-28 18:07:55 +01:00
self.coordinates = None
2019-12-27 18:31:54 +01:00
self.last_coordinates_update = end_time
def get_custom_location(self, request=None):
if request is not None:
self.request = request # todo: this is ugly, yes
return self.coordinates
@classmethod
def user_has_positions(cls, user):
if not user.is_authenticated:
return False
cache_key = 'user_has_positions:%d' % user.pk
result = cache.get(cache_key, None)
if result is None:
result = cls.objects.filter(owner=user).exists()
cache.set(cache_key, result, 600)
return result
def serialize_position(self, request=None):
# todo: make this pretty
custom_location = self.get_custom_location(request=request)
if custom_location is None:
return {
2019-12-27 23:27:50 +01:00
'id': 'p:%s' % self.secret,
'slug': 'p:%s' % self.secret,
'available': False,
'icon': 'my_location',
'title': self.name,
'subtitle': _('currently unavailable'),
}
from c3nav.mapdata.schemas.models import CustomLocationSchema
result = CustomLocationSchema.model_validate(custom_location).model_dump()
result.update({
'available': True,
'id': 'p:%s' % self.secret,
'slug': 'p:%s' % self.secret,
'icon': 'my_location',
'title': self.name,
'subtitle': '%s, %s, %s' % (
_('Position'),
result['title'],
result['subtitle']
),
})
return result
@property
def title(self):
return self.name
2019-12-27 23:27:50 +01:00
@property
def slug(self):
return 'p:%s' % self.secret
@property
def subtitle(self):
return _('Position')
@property
def icon(self):
return 'my_location'
@property
def effective_icon(self):
return self.icon
2024-12-03 16:07:07 +01:00
@property
def effective_slug(self):
return self.slug
2023-12-21 14:40:51 +01:00
def details_display(self, **kwargs):
return {
'id': self.pk,
'display': [
(_('Type'), self.__class__._meta.verbose_name),
(_('ID'), str(self.pk)),
(_('Title'), self.name),
(_('Slug'), self.slug),
(_('searchable'), _('No')),
(_('can describe'), _('No')),
(_('icon'), None),
],
}
2019-12-27 23:27:50 +01:00
def get_geometry(self, *args, **kwargs):
return None
level_id = None
def save(self, *args, **kwargs):
with transaction.atomic():
super().save(*args, **kwargs)
transaction.on_commit(lambda: cache.delete('user_has_positions:%d' % self.owner_id))
def delete(self, *args, **kwargs):
with transaction.atomic():
super().delete(*args, **kwargs)
transaction.on_commit(lambda: cache.delete('user_has_positions:%d' % self.owner_id))