From bc0b5521ce69d89ddbea068787f378fdf4f7b4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 14 Nov 2016 21:15:20 +0100 Subject: [PATCH] Refactoring Mapdata Models: MapItem and GeometryMapItem --- src/c3nav/api/urls.py | 11 +- src/c3nav/editor/forms.py | 2 +- src/c3nav/editor/views.py | 4 +- src/c3nav/mapdata/api.py | 142 ++++++++++++++++++ src/c3nav/mapdata/api/__init__.py | 0 src/c3nav/mapdata/api/features.py | 82 ---------- src/c3nav/mapdata/api/main.py | 66 -------- src/c3nav/mapdata/models/__init__.py | 3 +- src/c3nav/mapdata/models/base.py | 17 ++- .../models/{features.py => geometry.py} | 27 +--- src/c3nav/mapdata/models/level.py | 4 +- src/c3nav/mapdata/models/source.py | 4 +- src/c3nav/mapdata/packageio/const.py | 2 +- src/c3nav/mapdata/serializers/features.py | 14 +- 14 files changed, 186 insertions(+), 192 deletions(-) create mode 100644 src/c3nav/mapdata/api.py delete mode 100644 src/c3nav/mapdata/api/__init__.py delete mode 100644 src/c3nav/mapdata/api/features.py delete mode 100644 src/c3nav/mapdata/api/main.py rename src/c3nav/mapdata/models/{features.py => geometry.py} (86%) diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index 1f04aabe..ad60cf7c 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -8,18 +8,17 @@ from rest_framework.response import Response from rest_framework.routers import SimpleRouter from c3nav.editor.api import HosterViewSet, SubmitTaskViewSet -from c3nav.mapdata.api.features import (AreaViewSet, BuildingViewSet, DoorViewSet, FeatureTypeViewSet, FeatureViewSet, - ObstacleViewSet) -from c3nav.mapdata.api.main import LevelViewSet, PackageViewSet, SourceViewSet +from c3nav.mapdata.api import (AreaViewSet, BuildingViewSet, DoorViewSet, LevelViewSet, MapItemTypeViewSet, + MapItemViewSet, ObstacleViewSet, PackageViewSet, SourceViewSet) router = SimpleRouter() router.register(r'levels', LevelViewSet) router.register(r'packages', PackageViewSet) router.register(r'sources', SourceViewSet) -router.register(r'featuretypes', FeatureTypeViewSet, base_name='featuretype') -router.register(r'features', FeatureViewSet, base_name='features') -router.register(r'insides', BuildingViewSet) +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) diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index a18382c2..6248f310 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -9,7 +9,7 @@ from django.forms.widgets import HiddenInput from shapely.geometry.geo import mapping from c3nav.mapdata.models import Package -from c3nav.mapdata.models.features import Area, Building, Door, Obstacle +from c3nav.mapdata.models.geometry import Area, Building, Door, Obstacle from c3nav.mapdata.permissions import get_unlocked_packages diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index ee8e9c6a..f3b8ad60 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.features import FEATURE_TYPES +from c3nav.mapdata.models import 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 = FEATURE_TYPES.get(feature_type) + model = 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 new file mode 100644 index 00000000..65bd0b48 --- /dev/null +++ b/src/c3nav/mapdata/api.py @@ -0,0 +1,142 @@ +import mimetypes +import os +from collections import OrderedDict + +from django.conf import settings +from django.core.files import File +from django.http import Http404, 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.serializers.main import LevelSerializer, PackageSerializer, SourceSerializer + + +class MapItemTypeViewSet(ViewSet): + """ + List and retrieve feature types + """ + 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) + + +class MapItemViewSet(ViewSet): + """ + List all features. + This endpoint combines the list endpoints for all feature types. + """ + + 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) + + +class PackageViewSet(ReadOnlyModelViewSet): + """ + Retrieve packages the map consists of. + """ + queryset = Package.objects.all() + serializer_class = PackageSerializer + lookup_field = 'name' + lookup_value_regex = '[^/]+' + filter_fields = ('name', 'depends') + ordering_fields = ('name',) + ordering = ('name',) + search_fields = ('name',) + + +class LevelViewSet(ReadOnlyModelViewSet): + """ + List and retrieve levels. + """ + queryset = Level.objects.all() + serializer_class = LevelSerializer + lookup_field = 'name' + lookup_value_regex = '[^/]+' + filter_fields = ('altitude', 'package') + ordering_fields = ('altitude', 'package') + ordering = ('altitude',) + search_fields = ('name',) + + +class SourceViewSet(ReadOnlyModelViewSet): + """ + List and retrieve source images (to use as a drafts). + """ + queryset = Source.objects.all() + serializer_class = SourceSerializer + lookup_field = 'name' + lookup_value_regex = '[^/]+' + filter_fields = ('package',) + ordering_fields = ('name', 'package') + ordering = ('name',) + search_fields = ('name',) + + def get_queryset(self): + return filter_source_queryset(self.request, super().get_queryset()) + + @detail_route(methods=['get']) + def image(self, request, name=None): + source = self.get_object() + response = HttpResponse(content_type=mimetypes.guess_type(source.name)[0]) + image_path = os.path.join(settings.MAP_ROOT, source.package.directory, 'sources', source.name) + 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/api/__init__.py b/src/c3nav/mapdata/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/c3nav/mapdata/api/features.py b/src/c3nav/mapdata/api/features.py deleted file mode 100644 index 08f536d3..00000000 --- a/src/c3nav/mapdata/api/features.py +++ /dev/null @@ -1,82 +0,0 @@ -from collections import OrderedDict - -from django.http import Http404 -from rest_framework.response import Response -from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet - -from c3nav.mapdata.models import FEATURE_TYPES -from c3nav.mapdata.models.features import Area, Building, Door, Obstacle -from c3nav.mapdata.permissions import PackageAccessMixin -from c3nav.mapdata.serializers.features import (AreaSerializer, BuildingSerializer, DoorSerializer, - FeatureTypeSerializer, ObstacleSerializer) - - -class FeatureTypeViewSet(ViewSet): - """ - List and retrieve feature types - """ - lookup_field = 'name' - - def list(self, request): - serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request}) - return Response(serializer.data) - - def retrieve(self, request, pk=None): - if pk not in FEATURE_TYPES: - raise Http404 - serializer = FeatureTypeSerializer(FEATURE_TYPES[pk], context={'request': request}) - return Response(serializer.data) - - -class FeatureViewSet(ViewSet): - """ - List all features. - This endpoint combines the list endpoints for all feature types. - """ - - def list(self, request): - result = OrderedDict() - for name, model in FEATURE_TYPES.items(): - endpoint = model._meta.default_related_name - result[endpoint] = eval(model.__name__+'ViewSet').as_view({'get': 'list'})(request).data - return Response(result) - - -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/api/main.py b/src/c3nav/mapdata/api/main.py deleted file mode 100644 index e5ae1797..00000000 --- a/src/c3nav/mapdata/api/main.py +++ /dev/null @@ -1,66 +0,0 @@ -import mimetypes -import os - -from django.conf import settings -from django.core.files import File -from django.http import HttpResponse -from rest_framework.decorators import detail_route -from rest_framework.viewsets import ReadOnlyModelViewSet - -from c3nav.mapdata.models import Level, Package, Source -from c3nav.mapdata.permissions import filter_source_queryset -from c3nav.mapdata.serializers.main import LevelSerializer, PackageSerializer, SourceSerializer - - -class LevelViewSet(ReadOnlyModelViewSet): - """ - List and retrieve levels. - """ - queryset = Level.objects.all() - serializer_class = LevelSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - filter_fields = ('altitude', 'package') - ordering_fields = ('altitude', 'package') - ordering = ('altitude',) - search_fields = ('name',) - - -class PackageViewSet(ReadOnlyModelViewSet): - """ - Retrieve packages the map consists of. - """ - queryset = Package.objects.all() - serializer_class = PackageSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - filter_fields = ('name', 'depends') - ordering_fields = ('name',) - ordering = ('name',) - search_fields = ('name',) - - -class SourceViewSet(ReadOnlyModelViewSet): - """ - List and retrieve source images (to use as a drafts). - """ - queryset = Source.objects.all() - serializer_class = SourceSerializer - lookup_field = 'name' - lookup_value_regex = '[^/]+' - filter_fields = ('package',) - ordering_fields = ('name', 'package') - ordering = ('name',) - search_fields = ('name',) - - def get_queryset(self): - return filter_source_queryset(self.request, super().get_queryset()) - - @detail_route(methods=['get']) - def image(self, request, name=None): - source = self.get_object() - response = HttpResponse(content_type=mimetypes.guess_type(source.name)[0]) - image_path = os.path.join(settings.MAP_ROOT, source.package.directory, 'sources', source.name) - for chunk in File(open(image_path, 'rb')).chunks(): - response.write(chunk) - return response diff --git a/src/c3nav/mapdata/models/__init__.py b/src/c3nav/mapdata/models/__init__.py index d9e5d5c2..a51912a9 100644 --- a/src/c3nav/mapdata/models/__init__.py +++ b/src/c3nav/mapdata/models/__init__.py @@ -1,4 +1,5 @@ -from .features import Feature, FEATURE_TYPES # noqa +from .base import MAPITEM_TYPES # noqa from .level import Level # noqa from .package import Package # noqa from .source import Source # noqa +from .geometry import GeometryMapItem # noqa diff --git a/src/c3nav/mapdata/models/base.py b/src/c3nav/mapdata/models/base.py index 10f51efe..372e140e 100644 --- a/src/c3nav/mapdata/models/base.py +++ b/src/c3nav/mapdata/models/base.py @@ -1,13 +1,28 @@ 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 MapdataModel(models.Model): + +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): name = models.SlugField(_('Name'), unique=True, max_length=50) package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, verbose_name=_('map package')) + EditorForm = None + geomtype = None + color = None + @classmethod def get_path_prefix(cls): return cls._meta.default_related_name + '/' diff --git a/src/c3nav/mapdata/models/features.py b/src/c3nav/mapdata/models/geometry.py similarity index 86% rename from src/c3nav/mapdata/models/features.py rename to src/c3nav/mapdata/models/geometry.py index 6c60c2fa..f9b1b62c 100644 --- a/src/c3nav/mapdata/models/features.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -1,30 +1,19 @@ -from collections import OrderedDict - from django.db import models from django.utils.translation import ugettext_lazy as _ from shapely.geometry.geo import mapping, shape from c3nav.mapdata.fields import GeometryField -from c3nav.mapdata.models.base import MapdataModel +from c3nav.mapdata.models.base import MapItem from c3nav.mapdata.utils import format_geojson -FEATURE_TYPES = OrderedDict() - -def register_featuretype(cls): - FEATURE_TYPES[cls.__name__.lower()] = cls - return cls - - -class Feature(MapdataModel): +class GeometryMapItem(MapItem): """ A map feature """ level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level')) geometry = GeometryField() - EditorForm = None - class Meta: abstract = True @@ -56,8 +45,7 @@ class Feature(MapdataModel): return result -@register_featuretype -class Building(Feature): +class Building(GeometryMapItem): """ The outline of a building on a specific level """ @@ -70,8 +58,7 @@ class Building(Feature): default_related_name = 'buildings' -@register_featuretype -class Area(Feature): +class Area(GeometryMapItem): """ An accessible area like a room. Can also be outside. Can overlap. """ @@ -84,8 +71,7 @@ class Area(Feature): default_related_name = 'areas' -@register_featuretype -class Obstacle(Feature): +class Obstacle(GeometryMapItem): """ An obstacle """ @@ -117,8 +103,7 @@ class Obstacle(Feature): return result -@register_featuretype -class Door(Feature): +class Door(GeometryMapItem): """ A connection between two rooms """ diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index f35749c2..7826c1db 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -1,10 +1,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from c3nav.mapdata.models.base import MapdataModel +from c3nav.mapdata.models.base import MapItem -class Level(MapdataModel): +class Level(MapItem): """ A map level (-1, 0, 1, 2…) """ diff --git a/src/c3nav/mapdata/models/source.py b/src/c3nav/mapdata/models/source.py index 69dd7087..fbf07604 100644 --- a/src/c3nav/mapdata/models/source.py +++ b/src/c3nav/mapdata/models/source.py @@ -1,10 +1,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from c3nav.mapdata.models.base import MapdataModel +from c3nav.mapdata.models.base import MapItem -class Source(MapdataModel): +class Source(MapItem): """ A map source, images of levels that can be useful as backgrounds for the map editor """ diff --git a/src/c3nav/mapdata/packageio/const.py b/src/c3nav/mapdata/packageio/const.py index 93b83c97..da819fd6 100644 --- a/src/c3nav/mapdata/packageio/const.py +++ b/src/c3nav/mapdata/packageio/const.py @@ -1,4 +1,4 @@ from c3nav.mapdata.models import Level, Package, Source -from c3nav.mapdata.models.features import Area, Building +from c3nav.mapdata.models.geometry import Area, Building ordered_models = (Package, Level, Source, Building, Area) diff --git a/src/c3nav/mapdata/serializers/features.py b/src/c3nav/mapdata/serializers/features.py index 5544a4bb..91a3c65e 100644 --- a/src/c3nav/mapdata/serializers/features.py +++ b/src/c3nav/mapdata/serializers/features.py @@ -1,10 +1,10 @@ from rest_framework import serializers -from c3nav.mapdata.models.features import Area, Building, Door, Obstacle +from c3nav.mapdata.models.geometry import Area, Building, Door, Obstacle from c3nav.mapdata.serializers.fields import GeometryField -class FeatureTypeSerializer(serializers.Serializer): +class MapItemTypeSerializer(serializers.Serializer): name = serializers.SerializerMethodField() title = serializers.SerializerMethodField() title_plural = serializers.SerializerMethodField() @@ -29,31 +29,31 @@ class FeatureTypeSerializer(serializers.Serializer): return str(obj.__doc__.strip()) -class FeatureSerializer(serializers.ModelSerializer): +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(FeatureSerializer): +class BuildingSerializer(MapItemSerializer): class Meta: model = Building fields = ('name', 'level', 'package', 'geometry') -class AreaSerializer(FeatureSerializer): +class AreaSerializer(MapItemSerializer): class Meta: model = Area fields = ('name', 'level', 'package', 'geometry') -class ObstacleSerializer(FeatureSerializer): +class ObstacleSerializer(MapItemSerializer): class Meta: model = Obstacle fields = ('name', 'level', 'package', 'geometry', 'height') -class DoorSerializer(FeatureSerializer): +class DoorSerializer(MapItemSerializer): class Meta: model = Door fields = ('name', 'level', 'package', 'geometry')