From ed0f168a77bf17cbe6e6791496f1e9af75732861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 16 Dec 2016 11:03:40 +0100 Subject: [PATCH] move location stuff into the corresponding models (now renamed to Locations) --- src/c3nav/mapdata/api.py | 14 +- src/c3nav/mapdata/locations.py | 129 -------------- .../migrations/0018_auto_20161212_1205.py | 6 +- .../migrations/0019_auto_20161216_0923.py | 17 ++ .../migrations/0020_auto_20161216_0934.py | 44 +++++ src/c3nav/mapdata/models/__init__.py | 2 +- src/c3nav/mapdata/models/interest.py | 85 ---------- src/c3nav/mapdata/models/locations.py | 160 ++++++++++++++++++ src/c3nav/mapdata/packageio/const.py | 4 +- src/c3nav/mapdata/packageio/read.py | 6 +- src/c3nav/site/views.py | 2 +- 11 files changed, 236 insertions(+), 233 deletions(-) delete mode 100644 src/c3nav/mapdata/locations.py create mode 100644 src/c3nav/mapdata/migrations/0019_auto_20161216_0923.py create mode 100644 src/c3nav/mapdata/migrations/0020_auto_20161216_0934.py delete mode 100644 src/c3nav/mapdata/models/interest.py create mode 100644 src/c3nav/mapdata/models/locations.py diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index f700ee9a..3cb177b2 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -9,9 +9,9 @@ from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet -from c3nav.mapdata.locations import AreaOfInterestLocation, GroupOfInterestLocation, get_location -from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES, AreaOfInterest, GroupOfInterest, Level, Package, Source +from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES, AreaLocation, Level, LocationGroup, Package, Source from c3nav.mapdata.models.geometry import DirectedLineGeometryMapItemWithLevel +from c3nav.mapdata.models.locations import get_location from c3nav.mapdata.permissions import filter_queryset_by_package_access, get_unlocked_packages_names from c3nav.mapdata.serializers.main import LevelSerializer, PackageSerializer, SourceSerializer from c3nav.mapdata.utils.cache import (CachedReadOnlyViewSetMixin, cache_mapdata_api_response, get_levels_cached, @@ -161,13 +161,9 @@ class LocationViewSet(CachedReadOnlyViewSetMixin, ViewSet): def list(self, request, **kwargs): locations = [] - for area in filter_queryset_by_package_access(request, AreaOfInterest.objects.all()): - locations.append(AreaOfInterestLocation.from_cache(area)) - - for group in filter_queryset_by_package_access(request, GroupOfInterest.objects.all()): - locations.append(GroupOfInterestLocation.from_cache(group)) - - return Response([location.to_json() for location in locations]) + locations += list(filter_queryset_by_package_access(request, AreaLocation.objects.all())) + locations += list(filter_queryset_by_package_access(request, LocationGroup.objects.all())) + return Response([location.to_location_json() for location in locations]) def retrieve(self, request, name=None, **kwargs): location = get_location(request, name) diff --git a/src/c3nav/mapdata/locations.py b/src/c3nav/mapdata/locations.py deleted file mode 100644 index 97c0db57..00000000 --- a/src/c3nav/mapdata/locations.py +++ /dev/null @@ -1,129 +0,0 @@ -import re -from abc import ABC, abstractmethod -from collections import OrderedDict - -from django.core.cache import cache - -from c3nav.mapdata.lastupdate import get_last_mapdata_update -from c3nav.mapdata.models import AreaOfInterest, GroupOfInterest, Level -from c3nav.mapdata.permissions import filter_queryset_by_package_access -from c3nav.mapdata.utils.cache import get_levels_cached - - -def get_location(request, name): - match = re.match('^c:(?P[a-z0-9-_]+):(?P[0-9]+):(?P[0-9]+)$', name) - if match: - levels = get_levels_cached() - level = levels.get(match.group('level')) - if level is None: - return None - return PointLocation.from_cache(level=level, x=int(match.group('x')), y=int(match.group('y'))) - - if name.startswith('g:'): - group = filter_queryset_by_package_access(request, GroupOfInterest.objects.filter(name=name[2:])).first() - if group is None: - return None - return GroupOfInterestLocation(group) - - area = filter_queryset_by_package_access(request, AreaOfInterest.objects.filter(name=name)).first() - if area is None: - return None - return AreaOfInterestLocation(area) - - -class Location(ABC): - @classmethod - def _from_cache(cls, cache_key, *args, **kwargs): - last_update = get_last_mapdata_update() - if last_update is None: - return cls(*args, **kwargs) - - cache_key = 'c3nav__locations__%s__%s__%s' % (last_update.isoformat(), cls.__name__, cache_key) - obj = cache.get(cache_key) - if not obj: - obj = cls(*args, **kwargs) - cache.set(cache_key, obj, 300) - return obj - - def __init__(self, name): - self.name = name - - @property - @abstractmethod - def title(self) -> str: - pass - - @property - @abstractmethod - def subtitle(self) -> str: - pass - - def to_json(self): - return OrderedDict(( - ('name', self.name), - ('title', self.title), - ('subtitle', self.subtitle), - )) - - -class AreaOfInterestLocation(Location): - @classmethod - def from_cache(cls, area: AreaOfInterest): - return cls._from_cache(area.name, area) - - def __init__(self, area: AreaOfInterest): - super().__init__(name=area.name) - self.area = area - - @property - def title(self) -> str: - return self.area.title - - @property - def subtitle(self) -> str: - return 'Location Group' - - -class GroupOfInterestLocation(Location): - @classmethod - def from_cache(cls, group: GroupOfInterest): - return cls._from_cache(group.name, group) - - def __init__(self, group: GroupOfInterest): - super().__init__(name=group.name) - self.group = group - - @property - def title(self) -> str: - return self.group.title - - @property - def subtitle(self) -> str: - return 'Location' - - -class PointLocation(Location): - @classmethod - def from_cache(cls, level: Level, x: int, y: int): - return cls._from_cache('%s:%d:%d' % (level.name, x, y), level, x, y) - - def __init__(self, level: Level, x: int, y: int): - super().__init__(name='c:%s:%d:%d' % (level.name, x, y)) - self.level = level - self.x = x - self.y = y - - @property - def title(self) -> str: - return 'Custom location' - - @property - def subtitle(self) -> str: - return 'Coordinates' - - def to_json(self): - result = super().to_json() - result['level'] = self.level.name - result['x'] = self.x - result['y'] = self.y - return result diff --git a/src/c3nav/mapdata/migrations/0018_auto_20161212_1205.py b/src/c3nav/mapdata/migrations/0018_auto_20161212_1205.py index 6343d1f6..07803392 100644 --- a/src/c3nav/mapdata/migrations/0018_auto_20161212_1205.py +++ b/src/c3nav/mapdata/migrations/0018_auto_20161212_1205.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import c3nav.mapdata.fields -import c3nav.mapdata.models.interest +import c3nav.mapdata.models.locations from django.db import migrations, models import django.db.models.deletion @@ -28,7 +28,7 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Areas of Interest', 'verbose_name': 'Area of Interest', }, - bases=(models.Model, c3nav.mapdata.models.interest.MapItemOfInterestMixin), + bases=(models.Model, c3nav.mapdata.models.locations.LocationModelMixin), ), migrations.CreateModel( name='GroupOfInterest', @@ -43,7 +43,7 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Groups of Interest', 'verbose_name': 'Group of Interest', }, - bases=(models.Model, c3nav.mapdata.models.interest.MapItemOfInterestMixin), + bases=(models.Model, c3nav.mapdata.models.locations.LocationModelMixin), ), migrations.AddField( model_name='areaofinterest', diff --git a/src/c3nav/mapdata/migrations/0019_auto_20161216_0923.py b/src/c3nav/mapdata/migrations/0019_auto_20161216_0923.py new file mode 100644 index 00000000..01299171 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0019_auto_20161216_0923.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-16 09:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0018_auto_20161212_1205'), + ] + + operations = [ + migrations.RenameModel('AreaOfInterest', 'AreaLocation'), + migrations.RenameModel('GroupOfInterest', 'LocationGroup'), + ] diff --git a/src/c3nav/mapdata/migrations/0020_auto_20161216_0934.py b/src/c3nav/mapdata/migrations/0020_auto_20161216_0934.py new file mode 100644 index 00000000..261ede29 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0020_auto_20161216_0934.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-16 09:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0019_auto_20161216_0923'), + ] + + operations = [ + migrations.AlterModelOptions( + name='arealocation', + options={'verbose_name': 'Area Location', 'verbose_name_plural': 'Area Locations'}, + ), + migrations.AlterModelOptions( + name='locationgroup', + options={'verbose_name': 'Location Group', 'verbose_name_plural': 'Location Groups'}, + ), + migrations.AlterField( + model_name='arealocation', + name='groups', + field=models.ManyToManyField(blank=True, related_name='arealocations', to='mapdata.LocationGroup', verbose_name='Location Groups'), + ), + migrations.AlterField( + model_name='arealocation', + name='level', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arealocations', to='mapdata.Level', verbose_name='level'), + ), + migrations.AlterField( + model_name='arealocation', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arealocations', to='mapdata.Package', verbose_name='map package'), + ), + migrations.AlterField( + model_name='locationgroup', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locationgroups', to='mapdata.Package', verbose_name='map package'), + ), + ] diff --git a/src/c3nav/mapdata/models/__init__.py b/src/c3nav/mapdata/models/__init__.py index ad8be331..b2be04a2 100644 --- a/src/c3nav/mapdata/models/__init__.py +++ b/src/c3nav/mapdata/models/__init__.py @@ -3,4 +3,4 @@ from .package import Package # noqa from .source import Source # noqa from .collections import Elevator # noqa from .geometry import GeometryMapItemWithLevel, GEOMETRY_MAPITEM_TYPES # noqa -from .interest import AreaOfInterest, GroupOfInterest # noqa +from .locations import AreaLocation, LocationGroup # noqa diff --git a/src/c3nav/mapdata/models/interest.py b/src/c3nav/mapdata/models/interest.py deleted file mode 100644 index 64c80a50..00000000 --- a/src/c3nav/mapdata/models/interest.py +++ /dev/null @@ -1,85 +0,0 @@ -from collections import OrderedDict - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from c3nav.mapdata.fields import JSONField -from c3nav.mapdata.models.base import MapItem -from c3nav.mapdata.models.geometry import GeometryMapItemWithLevel - - -# noinspection PyUnresolvedReferences -class MapItemOfInterestMixin: - def get_geojson_properties(self): - result = super().get_geojson_properties() - result['titles'] = OrderedDict(sorted(self.titles.items())) - return result - - @classmethod - def fromfile(cls, data, file_path): - kwargs = super().fromfile(data, file_path) - - if 'titles' not in data: - raise ValueError('missing titles.') - titles = data['titles'] - if not isinstance(titles, dict): - raise ValueError('Invalid titles format.') - if any(not isinstance(lang, str) for lang in titles.keys()): - raise ValueError('titles: All languages have to be strings.') - if any(not isinstance(title, str) for title in titles.values()): - raise ValueError('titles: All titles have to be strings.') - if any(not title for title in titles.values()): - raise ValueError('titles: Titles must not be empty strings.') - kwargs['titles'] = titles - return kwargs - - def tofile(self): - result = super().tofile() - result['titles'] = OrderedDict(sorted(self.titles.items())) - return result - - -class GroupOfInterest(MapItemOfInterestMixin, MapItem): - titles = JSONField() - - class Meta: - verbose_name = _('Group of Interest') - verbose_name_plural = _('Groups of Interest') - default_related_name = 'groupsofinterest' - - -class AreaOfInterest(MapItemOfInterestMixin, GeometryMapItemWithLevel): - titles = JSONField() - groups = models.ManyToManyField(GroupOfInterest, verbose_name=_('Groups of Interest'), blank=True) - - geomtype = 'polygon' - - class Meta: - verbose_name = _('Area of Interest') - verbose_name_plural = _('Areas of Interest') - default_related_name = 'areasofinterest' - - @classmethod - def fromfile(cls, data, file_path): - kwargs = super().fromfile(data, file_path) - - groups = data.get('groups', []) - if not isinstance(groups, list): - raise TypeError('groups has to be a list') - kwargs['groups'] = groups - - return kwargs - - def get_geojson_properties(self): - result = super().get_geojson_properties() - result['groups'] = tuple(self.groups.all().order_by('name').values_list('name', flat=True)) - return result - - def tofile(self): - result = super().tofile() - result['groups'] = sorted(self.groups.all().order_by('name').values_list('name', flat=True)) - result.move_to_end('geometry') - return result - - def __str__(self): - return self.title diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py new file mode 100644 index 00000000..0a5fb7b7 --- /dev/null +++ b/src/c3nav/mapdata/models/locations.py @@ -0,0 +1,160 @@ +import re +from collections import OrderedDict + +from django.db import models +from django.utils.functional import cached_property +from django.utils.translation import ugettext_lazy as _ + +from c3nav.mapdata.fields import JSONField +from c3nav.mapdata.models import Level +from c3nav.mapdata.models.base import MapItem +from c3nav.mapdata.models.geometry import GeometryMapItemWithLevel +from c3nav.mapdata.permissions import filter_queryset_by_package_access +from c3nav.mapdata.utils.cache import get_levels_cached + + +class Location: + @property + def location_id(self): + raise NotImplementedError + + @property + def subtitle(self): + raise NotImplementedError + + def to_location_json(self): + return OrderedDict(( + ('id', self.location_id), + ('title', str(self.title)), + ('subtitle', str(self.subtitle)), + )) + + +# noinspection PyUnresolvedReferences +class LocationModelMixin(Location): + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['titles'] = OrderedDict(sorted(self.titles.items())) + return result + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + if 'titles' not in data: + raise ValueError('missing titles.') + titles = data['titles'] + if not isinstance(titles, dict): + raise ValueError('Invalid titles format.') + if any(not isinstance(lang, str) for lang in titles.keys()): + raise ValueError('titles: All languages have to be strings.') + if any(not isinstance(title, str) for title in titles.values()): + raise ValueError('titles: All titles have to be strings.') + if any(not title for title in titles.values()): + raise ValueError('titles: Titles must not be empty strings.') + kwargs['titles'] = titles + return kwargs + + def tofile(self): + result = super().tofile() + result['titles'] = OrderedDict(sorted(self.titles.items())) + return result + + @property + def subtitle(self): + return self._meta.verbose_name + + +class LocationGroup(LocationModelMixin, MapItem): + titles = JSONField() + + class Meta: + verbose_name = _('Location Group') + verbose_name_plural = _('Location Groups') + default_related_name = 'locationgroups' + + @cached_property + def location_id(self): + return 'g:'+self.name + + +class AreaLocation(LocationModelMixin, GeometryMapItemWithLevel): + titles = JSONField() + groups = models.ManyToManyField(LocationGroup, verbose_name=_('Location Groups'), blank=True) + + geomtype = 'polygon' + + class Meta: + verbose_name = _('Area Location') + verbose_name_plural = _('Area Locations') + default_related_name = 'arealocations' + + @cached_property + def location_id(self): + return self.name + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + groups = data.get('groups', []) + if not isinstance(groups, list): + raise TypeError('groups has to be a list') + kwargs['groups'] = groups + + return kwargs + + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['groups'] = tuple(self.groups.all().order_by('name').values_list('name', flat=True)) + return result + + def tofile(self): + result = super().tofile() + result['groups'] = sorted(self.groups.all().order_by('name').values_list('name', flat=True)) + result.move_to_end('geometry') + return result + + def __str__(self): + return self.title + + +def get_location(request, name): + match = re.match('^c:(?P[a-z0-9-_]+):(?P[0-9]+):(?P[0-9]+)$', name) + if match: + levels = get_levels_cached() + level = levels.get(match.group('level')) + if level is None: + return None + return PointLocation(level=level, x=int(match.group('x')), y=int(match.group('y'))) + + if name.startswith('g:'): + return filter_queryset_by_package_access(request, LocationGroup.objects.filter(name=name[2:])).first() + + return filter_queryset_by_package_access(request, AreaLocation.objects.filter(name=name)).first() + + +class PointLocation(Location): + def __init__(self, level: Level, x: int, y: int): + self.level = level + self.x = x + self.y = y + + @cached_property + def location_id(self): + return 'c:%s:%d:%d' % (self.level.name, self.x, self.y) + + @property + def title(self) -> str: + return 'Custom location' + + @property + def subtitle(self) -> str: + return 'Coordinates' + + def to_location_json(self): + result = super().to_location_json() + result['level'] = self.level.name + result['x'] = self.x + result['y'] = self.y + return result diff --git a/src/c3nav/mapdata/packageio/const.py b/src/c3nav/mapdata/packageio/const.py index 4e68f9c2..e1dbf69e 100644 --- a/src/c3nav/mapdata/packageio/const.py +++ b/src/c3nav/mapdata/packageio/const.py @@ -1,8 +1,8 @@ -from c3nav.mapdata.models import AreaOfInterest, GroupOfInterest, Level, Package, Source +from c3nav.mapdata.models import AreaLocation, Level, LocationGroup, Package, Source from c3nav.mapdata.models.collections import Elevator from c3nav.mapdata.models.geometry import (Building, Door, ElevatorLevel, Hole, LevelConnector, LineObstacle, Obstacle, Outside, Room, Stair) ordered_models = (Package, Level, LevelConnector, Source, Building, Room, Outside, Door, Obstacle, Hole) ordered_models += (Elevator, ElevatorLevel, LineObstacle, Stair) -ordered_models += (GroupOfInterest, AreaOfInterest) +ordered_models += (LocationGroup, AreaLocation) diff --git a/src/c3nav/mapdata/packageio/read.py b/src/c3nav/mapdata/packageio/read.py index 78b6dfbe..b51c9bae 100644 --- a/src/c3nav/mapdata/packageio/read.py +++ b/src/c3nav/mapdata/packageio/read.py @@ -7,7 +7,7 @@ from collections import OrderedDict from django.conf import settings from django.core.management import CommandError -from c3nav.mapdata.models import AreaOfInterest, Elevator, GroupOfInterest, Level, Package +from c3nav.mapdata.models import AreaLocation, Elevator, Level, LocationGroup, Package from c3nav.mapdata.models.geometry import LevelConnector from c3nav.mapdata.packageio.const import ordered_models @@ -166,8 +166,8 @@ class ReaderItem: self.data.pop('levels') groups = [] - if self.model == AreaOfInterest: - groups = [self.reader.saved_items[GroupOfInterest][name].obj.pk for name in self.data['groups']] + if self.model == AreaLocation: + groups = [self.reader.saved_items[LocationGroup][name].obj.pk for name in self.data['groups']] self.data.pop('groups') # Change name references to the referenced object diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index 7368eb4f..775365b9 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -1,6 +1,6 @@ from django.shortcuts import redirect, render -from c3nav.mapdata.locations import get_location +from c3nav.mapdata.models.locations import get_location def main(request, origin=None, destination=None):