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

335 lines
13 KiB
Python
Raw Normal View History

2017-10-27 15:05:41 +02:00
from collections import OrderedDict
from contextlib import suppress
2017-11-02 13:35:58 +01:00
from operator import attrgetter
from django.db import models
from django.db.models import Prefetch
2017-11-02 13:35:58 +01:00
from django.urls import reverse
2017-10-28 13:31:12 +02:00
from django.utils.functional import cached_property
from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
2017-11-30 16:51:28 +01:00
from c3nav.mapdata.fields import I18nField
from c3nav.mapdata.models.access import AccessRestrictionMixin
2017-07-10 13:01:35 +02:00
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
2017-06-22 19:27:51 +02:00
from c3nav.mapdata.utils.models import get_submodels
2017-05-11 19:36:49 +02: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:
result = result.select_related(*(model._meta.default_related_name
2017-06-22 19:27:51 +02:00
for model in get_submodels(Location)+[LocationRedirect]))
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('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
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()}
2017-07-08 16:50:37 +02:00
slug = models.SlugField(_('Slug'), unique=True, null=True, blank=True, max_length=50)
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-05-11 19:36:49 +02:00
def get_child(self):
2017-06-22 19:27:51 +02:00
for model in get_submodels(Location)+[LocationRedirect]:
with suppress(AttributeError):
2017-05-11 19:36:49 +02:00
return getattr(self, model._meta.default_related_name)
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
2017-11-02 13:35:58 +01:00
def details_display(self):
result = super().details_display()
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:
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'))
2017-05-10 18:03:57 +02:00
class Meta:
abstract = True
def serialize(self, detailed=True, describe_only=False, **kwargs):
result = super().serialize(detailed=detailed, **kwargs)
2017-05-12 13:21:41 +02:00
if not detailed:
if describe_only and not self.can_describe:
fields = ('id', 'on_top_of')
else:
fields = ('id', 'type', 'slug', 'title', 'subtitle', 'point', 'bounds', 'locations', 'on_top_of')
result = OrderedDict(((name, result[name]) for name in fields if name in result))
2017-05-12 13:21:41 +02:00
return result
2017-05-11 19:36:49 +02:00
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
result['subtitle'] = str(self.subtitle)
2017-05-11 19:36:49 +02:00
result['can_search'] = self.can_search
result['can_describe'] = self.can_search
2017-05-10 18:03:57 +02:00
return result
2017-11-02 13:35:58 +01:00
def details_display(self):
result = super().details_display()
result['display'].extend([
(_('searchable'), _('Yes') if self.can_search else _('No')),
(_('can describe'), _('Yes') if self.can_describe else _('No'))
2017-11-02 13:35:58 +01:00
])
return result
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 ''
def get_color(self, instance=None):
# dont filter in the query here so prefetch_related works
if instance is None:
instance = self
for group in instance.groups.all():
if group.color and getattr(group.category, 'allow_'+self.__class__._meta.default_related_name):
return group.color
return None
2017-05-13 16:39:01 +02:00
2017-05-10 18:03:57 +02:00
class SpecificLocation(Location, models.Model):
groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('Location Groups'), blank=True)
class Meta:
abstract = True
def _serialize(self, detailed=True, **kwargs):
result = super()._serialize(detailed=detailed, **kwargs)
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
2017-05-11 19:36:49 +02:00
return result
2017-11-02 13:35:58 +01:00
def details_display(self):
result = super().details_display()
groupcategories = {}
for group in self.groups.all():
groupcategories.setdefault(group.category, []).append(group)
for category, groups in sorted(groupcategories.items(), key=lambda item: item[0].priority):
result['display'].insert(3, (
category.title,
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
2017-10-27 13:47:12 +02:00
@property
def subtitle(self):
2017-12-14 21:44:14 +01:00
groups = tuple(self.groups.all() if 'groups' in getattr(self, '_prefetched_objects_cache', ()) else ())
return groups[0].title if groups else self.__class__._meta.verbose_name
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)
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)
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)
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
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
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 self.priority != self.orig_priority:
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):
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)
color = models.CharField(null=True, blank=True, max_length=32, verbose_name=_('background color'))
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
self.orig_category_id = self.category_id
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
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', ()))
return result
2017-11-02 13:35:58 +01:00
def details_display(self):
result = super().details_display()
result['display'].insert(3, (_('Category'), self.category.title))
2017-11-02 13:35:58 +01:00
result['display'].extend([
(_('color'), self.color),
(_('priority'), self.priority),
2017-11-02 13:35:58 +01:00
])
result['editor_url'] = reverse('editor.locationgroups.edit', kwargs={'pk': self.pk})
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=(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
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
self.category_id != self.orig_category_id):
self.register_changed_geometries()
super().save(*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'))
def _serialize(self, with_type=True, **kwargs):
result = super()._serialize(with_type=with_type, **kwargs)
if type(self.target) == LocationSlug:
result['target'] = self.target.get_child().slug
else:
result['target'] = self.target.slug
if with_type:
result['type'] = 'redirect'
result.pop('id')
return result
class Meta:
default_related_name = 'redirect'