2019-12-27 14:13:40 +01:00
|
|
|
import string
|
2017-05-31 02:38:59 +02:00
|
|
|
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
|
2017-05-31 02:38:59 +02:00
|
|
|
|
2019-12-27 14:13:40 +01:00
|
|
|
from django.conf import settings
|
2019-12-27 15:46:22 +01:00
|
|
|
from django.core.cache import cache
|
2019-12-22 00:45:11 +01:00
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
2019-12-27 15:46:22 +01:00
|
|
|
from django.db import models, transaction
|
2018-12-27 15:07:01 +01:00
|
|
|
from django.db.models import FieldDoesNotExist, 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
|
2017-10-28 20:30:27 +02:00
|
|
|
from django.utils.text import format_lazy
|
2016-12-16 11:03:40 +01:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2017-10-28 20:30:27 +02:00
|
|
|
from django.utils.translation import ungettext_lazy
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-11-30 16:51:28 +01:00
|
|
|
from c3nav.mapdata.fields import I18nField
|
2018-12-10 19:35:59 +01:00
|
|
|
from c3nav.mapdata.grid import grid
|
2017-07-13 18:43:03 +02:00
|
|
|
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
|
|
|
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-06-18 18:42:30 +02:00
|
|
|
class LocationSlugManager(models.Manager):
|
|
|
|
def get_queryset(self):
|
|
|
|
result = super().get_queryset()
|
|
|
|
if self.model == LocationSlug:
|
2018-12-27 15:07:01 +01:00
|
|
|
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')
|
2017-11-16 03:03:18 +01:00
|
|
|
return result
|
2017-06-18 18:42:30 +02:00
|
|
|
|
2017-07-19 15:20:12 +02:00
|
|
|
def select_related_target(self):
|
|
|
|
if self.model != LocationSlug:
|
|
|
|
raise TypeError
|
|
|
|
qs = self.get_queryset()
|
|
|
|
qs = qs.select_related('redirect__target', *('redirect__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'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2017-05-11 22:40:48 +02:00
|
|
|
class LocationSlug(SerializableMixin, models.Model):
|
|
|
|
LOCATION_TYPE_CODES = {
|
2017-06-11 14:43:14 +02:00
|
|
|
'Level': 'l',
|
|
|
|
'Space': 's',
|
2017-05-11 22:40:48 +02:00
|
|
|
'Area': 'a',
|
2017-07-08 16:29:12 +02:00
|
|
|
'POI': 'p',
|
2017-05-11 22:40:48 +02:00
|
|
|
'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
|
|
|
|
2017-12-24 13:23:19 +01:00
|
|
|
def get_child(self, instance=None):
|
2017-06-22 19:27:51 +02:00
|
|
|
for model in get_submodels(Location)+[LocationRedirect]:
|
2017-05-31 02:38:59 +02:00
|
|
|
with suppress(AttributeError):
|
2017-12-24 13:23:19 +01:00
|
|
|
return getattr(instance or self, model._meta.default_related_name)
|
2017-05-11 19:36:49 +02:00
|
|
|
return None
|
|
|
|
|
2017-05-12 12:54:10 +02:00
|
|
|
def get_slug(self):
|
|
|
|
return self.slug
|
|
|
|
|
|
|
|
def _serialize(self, **kwargs):
|
|
|
|
result = super()._serialize(**kwargs)
|
|
|
|
result['slug'] = self.get_slug()
|
|
|
|
return result
|
|
|
|
|
2018-09-19 19:08:47 +02:00
|
|
|
def details_display(self, **kwargs):
|
|
|
|
result = super().details_display(**kwargs)
|
2017-11-30 01:10:49 +01:00
|
|
|
result['display'].insert(2, (_('Slug'), str(self.get_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:
|
2017-05-27 18:29:36 +02:00
|
|
|
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'
|
|
|
|
|
|
|
|
|
2017-07-13 18:43:03 +02:00
|
|
|
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
|
|
|
|
|
2017-11-27 23:38:55 +01:00
|
|
|
def serialize(self, detailed=True, describe_only=False, **kwargs):
|
2017-06-22 19:53:25 +02:00
|
|
|
result = super().serialize(detailed=detailed, **kwargs)
|
2017-05-12 13:21:41 +02:00
|
|
|
if not detailed:
|
2018-12-21 03:07:07 +01:00
|
|
|
fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square',
|
2019-12-27 20:02:58 +01:00
|
|
|
'locations', 'on_top_of', 'label_settings', 'label_override', 'add_search', 'dynamic')
|
2018-12-27 16:07:07 +01:00
|
|
|
result = {name: result[name] for name in fields if name in result}
|
2017-05-12 13:21:41 +02:00
|
|
|
return result
|
|
|
|
|
2019-12-26 12:11:51 +01:00
|
|
|
def _serialize(self, search=False, **kwargs):
|
2017-05-11 19:36:49 +02:00
|
|
|
result = super()._serialize(**kwargs)
|
2017-10-28 20:30:27 +02:00
|
|
|
result['subtitle'] = str(self.subtitle)
|
2018-12-21 03:24:58 +01:00
|
|
|
result['icon'] = self.get_icon()
|
2017-05-11 19:36:49 +02:00
|
|
|
result['can_search'] = self.can_search
|
|
|
|
result['can_describe'] = self.can_search
|
2019-12-26 12:11:51 +01:00
|
|
|
if search:
|
|
|
|
result['add_search'] = ' '.join((
|
|
|
|
*(redirect.slug for redirect in self.redirects.all()),
|
|
|
|
*self.other_titles,
|
|
|
|
))
|
2017-05-10 18:03:57 +02:00
|
|
|
return result
|
|
|
|
|
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([
|
2017-11-30 01:10:49 +01:00
|
|
|
(_('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')),
|
|
|
|
(_('icon'), self.get_icon()),
|
2017-11-02 13:35:58 +01:00
|
|
|
])
|
|
|
|
return result
|
|
|
|
|
2017-05-11 22:40:48 +02:00
|
|
|
def get_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
|
2018-12-10 19:39:32 +01:00
|
|
|
def grid_square(self):
|
2018-12-10 19:06:38 +01:00
|
|
|
return None
|
|
|
|
|
2017-06-16 18:38:41 +02:00
|
|
|
def get_color(self, instance=None):
|
2018-12-21 19:06:29 +01:00
|
|
|
# dont filter in the query here so prefetch_related works
|
|
|
|
result = self.get_color_sorted(instance)
|
|
|
|
return None if result is None else result[1]
|
|
|
|
|
|
|
|
def get_color_sorted(self, instance=None):
|
2017-05-29 16:36:17 +02:00
|
|
|
# dont filter in the query here so prefetch_related works
|
2017-06-16 18:38:41 +02:00
|
|
|
if instance is None:
|
|
|
|
instance = self
|
2017-07-11 18:01:48 +02:00
|
|
|
for group in instance.groups.all():
|
2017-07-11 19:02:33 +02:00
|
|
|
if group.color and getattr(group.category, 'allow_'+self.__class__._meta.default_related_name):
|
2019-12-21 18:09:59 +01:00
|
|
|
return (0, group.category.priority, group.hierarchy, group.priority), group.color
|
2017-07-11 18:01:48 +02:00
|
|
|
return None
|
2017-05-13 16:39:01 +02:00
|
|
|
|
2018-12-21 03:07:07 +01:00
|
|
|
def get_icon(self):
|
|
|
|
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)
|
2017-05-10 18:03:57 +02:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2017-06-22 19:53:25 +02:00
|
|
|
def _serialize(self, detailed=True, **kwargs):
|
|
|
|
result = super()._serialize(detailed=detailed, **kwargs)
|
2018-12-10 19:35:59 +01:00
|
|
|
if grid.enabled:
|
2018-12-10 19:39:32 +01:00
|
|
|
grid_square = self.grid_square
|
|
|
|
if grid_square is not None:
|
2018-12-10 19:40:25 +01:00
|
|
|
result['grid_square'] = grid_square or None
|
2017-06-22 19:53:25 +02:00
|
|
|
if detailed:
|
2017-07-10 16:30:38 +02:00
|
|
|
groups = {}
|
|
|
|
for group in self.groups.all():
|
2017-07-11 18:53:43 +02:00
|
|
|
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)}
|
2017-07-10 16:30:38 +02:00
|
|
|
result['groups'] = groups
|
2019-12-22 00:38:54 +01:00
|
|
|
|
|
|
|
label_settings = self.get_label_settings()
|
|
|
|
if label_settings:
|
|
|
|
result['label_settings'] = label_settings.serialize(detailed=False)
|
|
|
|
if self.label_overrides:
|
|
|
|
# todo: what if only one language is set?
|
|
|
|
result['label_override'] = self.label_override
|
2017-05-11 19:36:49 +02:00
|
|
|
return result
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2019-12-22 00:38:54 +01:00
|
|
|
def get_label_settings(self):
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
|
2018-12-10 19:35:59 +01:00
|
|
|
if grid.enabled:
|
2018-12-10 19:39:32 +01:00
|
|
|
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):
|
2017-11-22 20:50:34 +01:00
|
|
|
result['display'].insert(3, (
|
2017-12-22 21:31:44 +01:00
|
|
|
category.title if category.single else category.title_plural,
|
2017-11-22 20:50:34 +01:00
|
|
|
tuple({
|
|
|
|
'id': group.pk,
|
|
|
|
'slug': group.get_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
|
|
|
|
|
|
|
return result
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
@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)
|
2019-12-27 20:02:58 +01:00
|
|
|
return groups
|
|
|
|
|
|
|
|
@property
|
|
|
|
def subtitle(self):
|
|
|
|
subtitle = self.describing_groups[0].title if self.describing_groups else self.__class__._meta.verbose_name
|
2018-12-10 19:39:32 +01:00
|
|
|
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)
|
|
|
|
|
2018-12-21 03:07:07 +01:00
|
|
|
def get_icon(self):
|
|
|
|
icon = super().get_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
|
|
|
|
2016-12-16 11:03:40 +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)
|
2017-07-10 18:55:35 +02:00
|
|
|
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='')
|
2017-07-10 18:55:35 +02:00
|
|
|
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)
|
2017-07-10 18:55:35 +02:00
|
|
|
priority = models.IntegerField(default=0, db_index=True)
|
2017-07-10 13:54:33 +02:00
|
|
|
|
2017-10-23 19:25:15 +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'
|
2017-07-10 19:16:35 +02:00
|
|
|
ordering = ('-priority', )
|
2017-07-10 13:54:33 +02:00
|
|
|
|
2017-11-30 16:51:28 +01:00
|
|
|
def _serialize(self, detailed=True, **kwargs):
|
|
|
|
result = super()._serialize(detailed=detailed, **kwargs)
|
2017-07-10 13:54:33 +02:00
|
|
|
result['name'] = self.name
|
2017-11-30 16:51:28 +01:00
|
|
|
if detailed:
|
|
|
|
result['titles'] = self.titles
|
|
|
|
result['title'] = self.title
|
2017-07-10 13:54:33 +02:00
|
|
|
return result
|
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
def register_changed_geometries(self):
|
|
|
|
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
|
2017-10-28 13:04:46 +02:00
|
|
|
query = self.groups.all()
|
2017-10-23 19:25:15 +02:00
|
|
|
for model in get_submodels(SpecificLocation):
|
2017-10-28 13:04:46 +02:00
|
|
|
related_name = model._meta.default_related_name
|
|
|
|
subquery = model.objects.all()
|
2017-10-23 19:25:15 +02:00
|
|
|
if issubclass(model, SpaceGeometryMixin):
|
2017-10-28 13:04:46 +02:00
|
|
|
subquery = subquery.select_related('space')
|
|
|
|
query.prefetch_related(Prefetch('groups__'+related_name, subquery))
|
2017-10-23 19:25:15 +02:00
|
|
|
|
|
|
|
for group in query:
|
|
|
|
group.register_changed_geometries(do_query=False)
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
2017-12-14 19:42:33 +01:00
|
|
|
if self.pk and self.priority != self.orig_priority:
|
2017-10-23 19:25:15 +02:00
|
|
|
self.register_changed_geometries()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
2017-07-10 13:54:33 +02:00
|
|
|
|
2017-07-11 19:02:09 +02:00
|
|
|
class LocationGroupManager(models.Manager):
|
|
|
|
def get_queryset(self):
|
|
|
|
return super().get_queryset().select_related('category')
|
|
|
|
|
|
|
|
|
2017-06-21 12:47:28 +02:00
|
|
|
class LocationGroup(Location, models.Model):
|
2017-07-10 13:54:33 +02:00
|
|
|
category = models.ForeignKey(LocationGroupCategory, related_name='groups', on_delete=models.PROTECT,
|
2017-07-10 14:10:48 +02:00
|
|
|
verbose_name=_('Category'))
|
2017-07-10 19:16:35 +02:00
|
|
|
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'))
|
2019-12-24 17:28:41 +01:00
|
|
|
can_report_missing = models.BooleanField(default=False, verbose_name=_('for missing locations'),
|
|
|
|
help_text=_('can be used when reporting a missing location'))
|
2017-09-16 14:19:01 +02:00
|
|
|
color = models.CharField(null=True, blank=True, max_length=32, verbose_name=_('background color'))
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-07-11 19:02:09 +02:00
|
|
|
objects = LocationGroupManager()
|
|
|
|
|
2016-12-16 11:03:40 +01:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Location Group')
|
|
|
|
verbose_name_plural = _('Location Groups')
|
|
|
|
default_related_name = 'locationgroups'
|
2017-07-11 17:41:16 +02:00
|
|
|
ordering = ('-category__priority', '-priority')
|
2016-12-16 11:03:40 +01:00
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.orig_priority = self.priority
|
2019-12-21 18:09:59 +01:00
|
|
|
self.orig_hierarchy = self.hierarchy
|
2017-10-27 13:47:23 +02:00
|
|
|
self.orig_category_id = self.category_id
|
2017-10-23 19:25:15 +02:00
|
|
|
self.orig_color = self.color
|
|
|
|
|
2017-10-28 21:36:52 +02:00
|
|
|
def _serialize(self, simple_geometry=False, **kwargs):
|
|
|
|
result = super()._serialize(simple_geometry=simple_geometry, **kwargs)
|
2017-07-10 13:54:33 +02:00
|
|
|
result['category'] = self.category_id
|
2017-07-13 13:32:06 +02:00
|
|
|
result['color'] = self.color
|
2017-10-28 21:36:52 +02:00
|
|
|
if simple_geometry:
|
|
|
|
result['locations'] = tuple(obj.pk for obj in getattr(self, 'locations', ()))
|
2016-12-23 22:42:42 +01:00
|
|
|
return result
|
|
|
|
|
2018-09-19 19:08:47 +02:00
|
|
|
def details_display(self, editor_url=True, **kwargs):
|
|
|
|
result = super().details_display(**kwargs)
|
2017-11-30 01:10:49 +01:00
|
|
|
result['display'].insert(3, (_('Category'), self.category.title))
|
2017-11-02 13:35:58 +01:00
|
|
|
result['display'].extend([
|
2017-11-30 01:10:49 +01:00
|
|
|
(_('color'), self.color),
|
|
|
|
(_('priority'), 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
|
|
|
|
|
2017-05-16 14:50:36 +02:00
|
|
|
@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)+')'
|
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
def register_changed_geometries(self, do_query=True):
|
|
|
|
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
|
|
|
|
for model in get_submodels(SpecificLocation):
|
2017-10-28 13:04:46 +02:00
|
|
|
query = getattr(self, model._meta.default_related_name).all()
|
2017-10-23 19:25:15 +02:00
|
|
|
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):
|
2017-10-28 20:30:27 +02:00
|
|
|
result = self.category.title
|
|
|
|
if hasattr(self, 'locations'):
|
|
|
|
return format_lazy(_('{category_title}, {num_locations}'),
|
|
|
|
category_title=result,
|
|
|
|
num_locations=(ungettext_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
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
def save(self, *args, **kwargs):
|
2017-12-14 19:47:07 +01:00
|
|
|
if self.pk and (self.orig_color != self.color or
|
|
|
|
self.priority != self.orig_priority or
|
2019-12-21 18:09:59 +01:00
|
|
|
self.hierarchy != self.orig_hierarchy or
|
2017-12-14 19:47:07 +01:00
|
|
|
self.category_id != self.orig_category_id):
|
2017-10-23 19:25:15 +02:00
|
|
|
self.register_changed_geometries()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
2017-12-23 03:19:52 +01:00
|
|
|
def delete(self, *args, **kwargs):
|
|
|
|
self.register_changed_geometries()
|
|
|
|
super().delete(*args, **kwargs)
|
|
|
|
|
2017-05-16 14:50:36 +02:00
|
|
|
|
2017-05-11 22:40:48 +02:00
|
|
|
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'))
|
2017-05-11 22:40:48 +02:00
|
|
|
|
2017-12-17 03:24:12 +01:00
|
|
|
def _serialize(self, include_type=True, **kwargs):
|
|
|
|
result = super()._serialize(include_type=include_type, **kwargs)
|
2017-05-11 22:40:48 +02:00
|
|
|
if type(self.target) == LocationSlug:
|
|
|
|
result['target'] = self.target.get_child().slug
|
|
|
|
else:
|
|
|
|
result['target'] = self.target.slug
|
2017-12-17 03:24:12 +01:00
|
|
|
if include_type:
|
2017-05-11 22:40:48 +02:00
|
|
|
result['type'] = 'redirect'
|
|
|
|
result.pop('id')
|
|
|
|
return result
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
default_related_name = 'redirect'
|
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)])
|
|
|
|
|
|
|
|
def _serialize(self, detailed=True, **kwargs):
|
|
|
|
result = super()._serialize(detailed=detailed, **kwargs)
|
|
|
|
if detailed:
|
|
|
|
result['titles'] = self.titles
|
|
|
|
if self.min_zoom > -10:
|
|
|
|
result['min_zoom'] = self.min_zoom
|
|
|
|
if self.max_zoom < 10:
|
|
|
|
result['max_zoom'] = self.max_zoom
|
|
|
|
result['font_size'] = self.font_size
|
|
|
|
return result
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
class CustomLocationProxyMixin:
|
|
|
|
def get_custom_location(self):
|
|
|
|
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
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
def serialize_position(self):
|
|
|
|
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 = 'dynamic_locations'
|
|
|
|
|
|
|
|
def _serialize(self, **kwargs):
|
2019-12-27 20:02:58 +01:00
|
|
|
"""custom_location = self.get_custom_location()
|
|
|
|
print(custom_location)
|
|
|
|
result = {} if custom_location is None else custom_location.serialize(**kwargs)
|
|
|
|
super_result = super()._serialize(**kwargs)
|
|
|
|
super_result['subtitle'] = '%s %s, %s' % (_('(moving)'), result['title'], result['subtitle'])
|
|
|
|
result.update(super_result)"""
|
2019-12-27 14:13:40 +01:00
|
|
|
result = super()._serialize(**kwargs)
|
2019-12-27 20:02:58 +01:00
|
|
|
result['dynamic'] = True
|
2019-12-27 14:13:40 +01:00
|
|
|
return result
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
def serialize_position(self):
|
|
|
|
custom_location = self.get_custom_location()
|
|
|
|
if custom_location is None:
|
|
|
|
return {
|
|
|
|
'available': False,
|
|
|
|
'title': self.title,
|
|
|
|
'subtitle': '%s %s, %s' % (_('currently unavailable'), _('(moving)'), self.subtitle)
|
|
|
|
}
|
2019-12-27 20:36:22 +01:00
|
|
|
result = custom_location.serialize(simple_geometry=True)
|
2019-12-27 20:02:58 +01:00
|
|
|
result.update({
|
|
|
|
'available': True,
|
|
|
|
'id': self.pk,
|
|
|
|
'slug': self.slug,
|
|
|
|
'coordinates': custom_location.pk,
|
|
|
|
'icon': self.get_icon(),
|
|
|
|
'title': 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):
|
|
|
|
if not self.position_secret:
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
return Position.objects.get(secret=self.position_secret).get_custom_location()
|
|
|
|
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:
|
2019-12-27 20:02:58 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
def get_position_api_secret():
|
|
|
|
return get_random_string(64, string.ascii_letters+string.digits)
|
|
|
|
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
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-27 18:31:54 +01:00
|
|
|
timeout = models.PositiveSmallIntegerField(_('timeout (in seconds)'), default=0, help_text=_('0 for no timeout'))
|
2019-12-27 16:51:06 +01:00
|
|
|
coordinates_id = models.CharField(_('coordinates'), null=True, max_length=48)
|
2019-12-27 14:13:40 +01:00
|
|
|
api_secret = models.CharField(_('api secret'), max_length=64, default=get_position_api_secret)
|
|
|
|
|
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 15:46:22 +01:00
|
|
|
|
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:
|
|
|
|
self.cordinates = None
|
|
|
|
self.last_coordinates_update = end_time
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
def get_custom_location(self):
|
|
|
|
return self.coordinates
|
|
|
|
|
2019-12-27 15:46:22 +01:00
|
|
|
@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
|
|
|
|
|
2019-12-27 20:02:58 +01:00
|
|
|
def serialize_position(self):
|
|
|
|
custom_location = self.get_custom_location()
|
|
|
|
if custom_location is None:
|
|
|
|
return {
|
2019-12-27 23:27:50 +01:00
|
|
|
'id': 'p:%s' % self.secret,
|
|
|
|
'slug': 'p:%s' % self.secret,
|
2019-12-27 20:02:58 +01:00
|
|
|
'available': False,
|
|
|
|
'icon': 'my_location',
|
|
|
|
'title': self.name,
|
|
|
|
'subtitle': _('currently unavailable'),
|
|
|
|
}
|
2019-12-27 20:36:22 +01:00
|
|
|
result = custom_location.serialize(simple_geometry=True)
|
2019-12-27 20:02:58 +01:00
|
|
|
result.update({
|
|
|
|
'available': True,
|
|
|
|
'id': 'p:%s' % self.secret,
|
|
|
|
'slug': 'p:%s' % self.secret,
|
|
|
|
'coordinates': custom_location.pk,
|
|
|
|
'icon': 'my_location',
|
|
|
|
'title': self.name,
|
|
|
|
'subtitle': '%s, %s, %s' % (
|
|
|
|
_('Position'),
|
|
|
|
result['title'],
|
|
|
|
result['subtitle']
|
|
|
|
),
|
|
|
|
})
|
|
|
|
return result
|
|
|
|
|
2019-12-27 23:27:50 +01:00
|
|
|
@property
|
|
|
|
def slug(self):
|
|
|
|
return 'p:%s' % self.secret
|
|
|
|
|
|
|
|
def serialize(self, *args, **kwargs):
|
|
|
|
return self.serialize_position()
|
|
|
|
|
|
|
|
def get_geometry(self, *args, **kwargs):
|
|
|
|
return None
|
|
|
|
|
|
|
|
level_id = None
|
|
|
|
|
2019-12-27 15:46:22 +01:00
|
|
|
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))
|