From 769343f78a58589a98f59860d806e860679786b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 27 Nov 2016 14:03:39 +0100 Subject: [PATCH] change mapitems API to geometry API. --- src/c3nav/api/urls.py | 13 +-- src/c3nav/editor/forms.py | 2 +- src/c3nav/editor/views.py | 4 +- src/c3nav/mapdata/api.py | 105 ++++++++-------------- src/c3nav/mapdata/models/__init__.py | 2 +- src/c3nav/mapdata/models/base.py | 13 +-- src/c3nav/mapdata/models/geometry.py | 41 ++++++++- src/c3nav/mapdata/permissions.py | 7 +- src/c3nav/mapdata/serializers/features.py | 59 ------------ 9 files changed, 88 insertions(+), 158 deletions(-) delete mode 100644 src/c3nav/mapdata/serializers/features.py diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index ad60cf7c..32f17c98 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -8,20 +8,15 @@ from rest_framework.response import Response from rest_framework.routers import SimpleRouter from c3nav.editor.api import HosterViewSet, SubmitTaskViewSet -from c3nav.mapdata.api import (AreaViewSet, BuildingViewSet, DoorViewSet, LevelViewSet, MapItemTypeViewSet, - MapItemViewSet, ObstacleViewSet, PackageViewSet, SourceViewSet) +from c3nav.mapdata.api import GeometryStyleViewSet, GeometryViewSet, LevelViewSet, PackageViewSet, SourceViewSet router = SimpleRouter() -router.register(r'levels', LevelViewSet) router.register(r'packages', PackageViewSet) +router.register(r'levels', LevelViewSet) router.register(r'sources', SourceViewSet) -router.register(r'mapitemtypes', MapItemTypeViewSet, base_name='mapitemtype') -router.register(r'mapitems', MapItemViewSet, base_name='mapitem') -router.register(r'buildings', BuildingViewSet) -router.register(r'rooms', AreaViewSet) -router.register(r'obstacles', ObstacleViewSet) -router.register(r'doors', DoorViewSet) +router.register(r'geometrystyles', GeometryStyleViewSet, base_name='geometrystyle') +router.register(r'geometries', GeometryViewSet, base_name='geometry') router.register(r'hosters', HosterViewSet, base_name='hoster') router.register(r'submittasks', SubmitTaskViewSet, base_name='submittask') diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 6248f310..79954fe8 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -23,7 +23,7 @@ class FeatureFormMixin(ModelForm): if not creating and not settings.DIRECT_EDITING: self.fields['name'].disabled = True - if creating and self._meta.model in (Door, ): + if creating and self._meta.model in (Door, Obstacle, ): self.fields['name'].initial = uuid.uuid4() # restrict package choices and field_name diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index f3b8ad60..42b28bba 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -7,14 +7,14 @@ from django.shortcuts import get_object_or_404, render from django.utils import translation from c3nav.editor.hosters import get_hoster_for_package, hosters -from c3nav.mapdata.models import MAPITEM_TYPES +from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES from c3nav.mapdata.models.package import Package from c3nav.mapdata.packageio.write import json_encode from c3nav.mapdata.permissions import can_access_package def edit_feature(request, feature_type, name=None): - model = MAPITEM_TYPES.get(feature_type) + model = GEOMETRY_MAPITEM_TYPES.get(feature_type) if model is None: raise Http404() diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 65bd0b48..a22d99e3 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -4,48 +4,59 @@ from collections import OrderedDict from django.conf import settings from django.core.files import File -from django.http import Http404, HttpResponse +from django.http import HttpResponse from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet -from c3nav.mapdata.models import MAPITEM_TYPES, Level, Package, Source -from c3nav.mapdata.models.geometry import Area, Building, Door, Obstacle -from c3nav.mapdata.permissions import PackageAccessMixin, filter_source_queryset -from c3nav.mapdata.serializers.features import (AreaSerializer, BuildingSerializer, DoorSerializer, - MapItemTypeSerializer, ObstacleSerializer) +from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES, Level, Package, Source +from c3nav.mapdata.permissions import filter_queryset_by_package_access from c3nav.mapdata.serializers.main import LevelSerializer, PackageSerializer, SourceSerializer -class MapItemTypeViewSet(ViewSet): +class GeometryStyleViewSet(ViewSet): """ - List and retrieve feature types + List all geometry styles. """ - lookup_field = 'name' - def list(self, request): - serializer = MapItemTypeSerializer(MAPITEM_TYPES.values(), many=True, context={'request': request}) - return Response(serializer.data) - - def retrieve(self, request, pk=None): - if pk not in MAPITEM_TYPES: - raise Http404 - serializer = MapItemTypeSerializer(MAPITEM_TYPES[pk], context={'request': request}) - return Response(serializer.data) + styles = OrderedDict() + for mapitemtype in GEOMETRY_MAPITEM_TYPES.values(): + styles.update(mapitemtype.get_styles()) + return Response(styles) -class MapItemViewSet(ViewSet): +class GeometryViewSet(ViewSet): """ - List all features. - This endpoint combines the list endpoints for all feature types. + List all geometries. + You can filter by adding one or more level, package, type or name GET parameters. """ def list(self, request): - result = OrderedDict() - for name, model in MAPITEM_TYPES.items(): - endpoint = model._meta.default_related_name - result[endpoint] = eval(model.__name__+'ViewSet').as_view({'get': 'list'})(request).data - return Response(result) + types = request.GET.getlist('type') + valid_types = list(GEOMETRY_MAPITEM_TYPES.keys()) + if not types: + types = valid_types + else: + types = [t for t in types if t in valid_types] + + levels = request.GET.getlist('levels') + packages = request.GET.getlist('package') + names = request.GET.getlist('name') + + results = [] + for t in types: + mapitemtype = GEOMETRY_MAPITEM_TYPES[t] + queryset = mapitemtype.objects.all() + if packages: + queryset = queryset.filter(package__name__in=packages) + if levels: + queryset = queryset.filter(level__name__in=levels) + if names: + queryset = queryset.filter(name__in=names) + queryset = filter_queryset_by_package_access(request, queryset) + queryset.prefetch_related('package', 'level').order_by('name') + results.extend(sum((obj.to_geojson() for obj in queryset), [])) + return Response(results) class PackageViewSet(ReadOnlyModelViewSet): @@ -90,7 +101,7 @@ class SourceViewSet(ReadOnlyModelViewSet): search_fields = ('name',) def get_queryset(self): - return filter_source_queryset(self.request, super().get_queryset()) + return filter_queryset_by_package_access(self.request, super().get_queryset()) @detail_route(methods=['get']) def image(self, request, name=None): @@ -100,43 +111,3 @@ class SourceViewSet(ReadOnlyModelViewSet): for chunk in File(open(image_path, 'rb')).chunks(): response.write(chunk) return response - - -class BuildingViewSet(PackageAccessMixin, ReadOnlyModelViewSet): - """ - List and retrieve Inside Areas - """ - queryset = Building.objects.all() - serializer_class = BuildingSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - - -class AreaViewSet(PackageAccessMixin, ReadOnlyModelViewSet): - """ - List and retrieve Areas - """ - queryset = Area.objects.all() - serializer_class = AreaSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - - -class ObstacleViewSet(PackageAccessMixin, ReadOnlyModelViewSet): - """ - List and retrieve Obstcales - """ - queryset = Obstacle.objects.all() - serializer_class = ObstacleSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - - -class DoorViewSet(PackageAccessMixin, ReadOnlyModelViewSet): - """ - List and retrieve Doors - """ - queryset = Door.objects.all() - serializer_class = DoorSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' diff --git a/src/c3nav/mapdata/models/__init__.py b/src/c3nav/mapdata/models/__init__.py index a51912a9..9fd10c3c 100644 --- a/src/c3nav/mapdata/models/__init__.py +++ b/src/c3nav/mapdata/models/__init__.py @@ -1,4 +1,4 @@ -from .base import MAPITEM_TYPES # noqa +from c3nav.mapdata.models.geometry import GEOMETRY_MAPITEM_TYPES # noqa from .level import Level # noqa from .package import Package # noqa from .source import Source # noqa diff --git a/src/c3nav/mapdata/models/base.py b/src/c3nav/mapdata/models/base.py index 372e140e..d2ea7afc 100644 --- a/src/c3nav/mapdata/models/base.py +++ b/src/c3nav/mapdata/models/base.py @@ -1,21 +1,10 @@ from collections import OrderedDict from django.db import models -from django.db.models.base import ModelBase from django.utils.translation import ugettext_lazy as _ -MAPITEM_TYPES = OrderedDict() - -class MapItemMeta(ModelBase): - def __new__(mcs, name, bases, attrs): - cls = super().__new__(mcs, name, bases, attrs) - if not cls._meta.abstract: - MAPITEM_TYPES[name.lower()] = cls - return cls - - -class MapItem(models.Model, metaclass=MapItemMeta): +class MapItem(models.Model): name = models.SlugField(_('Name'), unique=True, max_length=50) package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, verbose_name=_('map package')) diff --git a/src/c3nav/mapdata/models/geometry.py b/src/c3nav/mapdata/models/geometry.py index f9b1b62c..bbee2330 100644 --- a/src/c3nav/mapdata/models/geometry.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -1,4 +1,7 @@ +from collections import OrderedDict + from django.db import models +from django.db.models.base import ModelBase from django.utils.translation import ugettext_lazy as _ from shapely.geometry.geo import mapping, shape @@ -6,8 +9,18 @@ from c3nav.mapdata.fields import GeometryField from c3nav.mapdata.models.base import MapItem from c3nav.mapdata.utils import format_geojson +GEOMETRY_MAPITEM_TYPES = OrderedDict() -class GeometryMapItem(MapItem): + +class GeometryMapItemMeta(ModelBase): + def __new__(mcs, name, bases, attrs): + cls = super().__new__(mcs, name, bases, attrs) + if not cls._meta.abstract: + GEOMETRY_MAPITEM_TYPES[name.lower()] = cls + return cls + + +class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): """ A map feature """ @@ -38,6 +51,27 @@ class GeometryMapItem(MapItem): return kwargs + @classmethod + def get_styles(cls): + return { + cls.__name__.lower(): cls.color + } + + def get_geojson_properties(self): + return OrderedDict(( + ('type', self.__class__.__name__.lower()), + ('name', self.name), + ('package', self.package.name), + ('level', self.level.name), + )) + + def to_geojson(self): + return [OrderedDict(( + ('type', 'Feature'), + ('properties', self.get_geojson_properties()), + ('geometry', format_geojson(mapping(self.geometry), round=False)), + ))] + def tofile(self): result = super().tofile() result['level'] = self.level.name @@ -85,6 +119,11 @@ class Obstacle(GeometryMapItem): verbose_name_plural = _('Obstacles') default_related_name = 'obstacles' + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['height'] = float(self.height) + return result + @classmethod def fromfile(cls, data, file_path): kwargs = super().fromfile(data, file_path) diff --git a/src/c3nav/mapdata/permissions.py b/src/c3nav/mapdata/permissions.py index b0ca5524..bf224622 100644 --- a/src/c3nav/mapdata/permissions.py +++ b/src/c3nav/mapdata/permissions.py @@ -14,7 +14,7 @@ def can_access_package(request, package): return settings.DEBUG or package.name in get_unlocked_packages(request) -def filter_source_queryset(request, queryset): +def filter_queryset_by_package_access(request, queryset): return queryset if settings.DIRECT_EDITING else queryset.filter(package__name__in=get_unlocked_packages(request)) @@ -24,8 +24,3 @@ class LockedMapFeatures(BasePermission): if not can_access_package(request, obj.package): raise PermissionDenied(_('This Source belongs to a package you don\'t have access to.')) return True - - -class PackageAccessMixin: - def get_queryset(self): - return filter_source_queryset(self.request, super().get_queryset()) diff --git a/src/c3nav/mapdata/serializers/features.py b/src/c3nav/mapdata/serializers/features.py deleted file mode 100644 index 91a3c65e..00000000 --- a/src/c3nav/mapdata/serializers/features.py +++ /dev/null @@ -1,59 +0,0 @@ -from rest_framework import serializers - -from c3nav.mapdata.models.geometry import Area, Building, Door, Obstacle -from c3nav.mapdata.serializers.fields import GeometryField - - -class MapItemTypeSerializer(serializers.Serializer): - name = serializers.SerializerMethodField() - title = serializers.SerializerMethodField() - title_plural = serializers.SerializerMethodField() - endpoint = serializers.SerializerMethodField() - description = serializers.SerializerMethodField() - geomtype = serializers.CharField() - color = serializers.CharField() - - def get_name(self, obj): - return obj.__name__.lower() - - def get_title(self, obj): - return str(obj._meta.verbose_name) - - def get_title_plural(self, obj): - return str(obj._meta.verbose_name_plural) - - def get_endpoint(self, obj): - return obj._meta.default_related_name - - def get_description(self, obj): - return str(obj.__doc__.strip()) - - -class MapItemSerializer(serializers.ModelSerializer): - level = serializers.SlugRelatedField(slug_field='name', read_only=True) - package = serializers.SlugRelatedField(slug_field='name', read_only=True) - geometry = GeometryField() - - -class BuildingSerializer(MapItemSerializer): - class Meta: - model = Building - fields = ('name', 'level', 'package', 'geometry') - - -class AreaSerializer(MapItemSerializer): - class Meta: - model = Area - fields = ('name', 'level', 'package', 'geometry') - - -class ObstacleSerializer(MapItemSerializer): - class Meta: - model = Obstacle - fields = ('name', 'level', 'package', 'geometry', 'height') - - -class DoorSerializer(MapItemSerializer): - class Meta: - model = Door - fields = ('name', 'level', 'package', 'geometry')