diff --git a/src/c3nav/api/serializers.py b/src/c3nav/api/serializers.py new file mode 100644 index 00000000..2e5e3a24 --- /dev/null +++ b/src/c3nav/api/serializers.py @@ -0,0 +1,46 @@ +from collections import Iterable + +from django.db.models.manager import BaseManager +from rest_framework import serializers + + +class PkField(serializers.DictField): + """ + give primary key + """ + def to_representation(self, obj): + if hasattr(obj, 'pk'): + return obj.pk + elif isinstance(obj, Iterable): + return tuple(self.to_representation(elem) for elem in obj) + elif isinstance(obj, BaseManager): + return tuple(self.to_representation(elem) for elem in obj.all()) + return None + + +class RecursiveSerializerMixin(serializers.Serializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + request = self.context.get('request') + request_sparse = self.context['request_sparse'] = request is not None and request.GET.get('sparse') + sparse = self.context['sparse'] = request_sparse or self.context.get('sparse') + + if sparse: + for name in getattr(self.Meta, 'sparse_exclude', ()): + value = self.fields.get(name) + if value is not None and isinstance(value, serializers.Serializer): + self.fields[name] = PkField() + + if request_sparse: + for name in tuple(self.fields): + if name == 'url' or name.endswith('_url'): + self.fields.pop(name) + + def sparse_context(self): + return {'request': self.context.get('request'), 'sparse': True} + + def recursive_value(self, serializer, obj, *args, **kwargs): + if self.context.get('sparse'): + return PkField().to_representation(obj) + return serializer(obj, *args, **kwargs, context=self.sparse_context()).data diff --git a/src/c3nav/api/templates/rest_framework/api.html b/src/c3nav/api/templates/rest_framework/api.html index dd546452..cc6a6094 100644 --- a/src/c3nav/api/templates/rest_framework/api.html +++ b/src/c3nav/api/templates/rest_framework/api.html @@ -29,5 +29,6 @@ {% block description %} {% if breadcrumblist|length == 1 %}

Welcome to the c3nav RESTful API.

+

Add ?sparse=1 to any request to flatten its result.

{% else %}{{ description }}{% endif %} {% endblock %} diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index 2dd4567b..fa305378 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -16,12 +16,12 @@ from c3nav.mapdata.models.package import Package class HosterViewSet(ViewSet): """ - Get Package Hosters + Retrieve and interact with package hosters """ def retrieve(self, request, pk=None): if pk not in hosters: raise Http404 - serializer = HosterSerializer(hosters[pk]) + serializer = HosterSerializer(hosters[pk], context={'request': request}) return Response(serializer.data) @detail_route(methods=['get']) @@ -90,7 +90,7 @@ class HosterViewSet(ViewSet): class SubmitTaskViewSet(ViewSet): """ - Get Submit Tasks + Get hoster submit tasks """ def retrieve(self, request, pk=None): task = submit_edit_task.AsyncResult(task_id=pk) diff --git a/src/c3nav/editor/hosters/base.py b/src/c3nav/editor/hosters/base.py index c4c24467..3efc8bcc 100644 --- a/src/c3nav/editor/hosters/base.py +++ b/src/c3nav/editor/hosters/base.py @@ -15,11 +15,15 @@ class Hoster(ABC): self.name = name self.base_url = base_url + @property + def pk(self): + return self.name + def get_packages(self): """ Get a Queryset of all packages that can be handled by this hoster """ - return Package.objects.filter(home_repo__startswith=self.base_url) + return Package.objects.filter(home_repo__startswith=self.base_url).order_by('name') def _get_callback_uri(self, request): uri = request.build_absolute_uri(reverse('editor.oauth.callback', kwargs={'hoster': self.name})) diff --git a/src/c3nav/editor/serializers.py b/src/c3nav/editor/serializers.py index 6b6f9330..91c9deb2 100644 --- a/src/c3nav/editor/serializers.py +++ b/src/c3nav/editor/serializers.py @@ -1,17 +1,28 @@ from rest_framework import serializers +from rest_framework.reverse import reverse class HosterSerializer(serializers.Serializer): name = serializers.CharField() + url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail') + state_url = serializers.SerializerMethodField() + auth_uri_url = serializers.SerializerMethodField() + submit_url = serializers.SerializerMethodField() base_url = serializers.CharField() - packages = serializers.SerializerMethodField() - def get_packages(self, obj): - return tuple(obj.get_packages().values_list('name', flat=True)) + def get_state_url(self, obj): + return reverse('api:hoster-state', args=(obj.name, ), request=self.context.get('request')) + + def get_auth_uri_url(self, obj): + return reverse('api:hoster-auth-uri', args=(obj.name, ), request=self.context.get('request')) + + def get_submit_url(self, obj): + return reverse('api:hoster-submit', args=(obj.name, ), request=self.context.get('request')) class TaskSerializer(serializers.Serializer): id = serializers.CharField() + url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail', lookup_field='id', lookup_url_kwarg='pk') started = serializers.SerializerMethodField() done = serializers.SerializerMethodField() success = serializers.SerializerMethodField() diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 7279ba73..1241c9c4 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -51,7 +51,7 @@ editor = { packages: {}, get_packages: function () { - $.getJSON('/api/packages/', function (packages) { + $.getJSON('/api/packages/?sparse=1', function (packages) { var bounds = [[0, 0], [0, 0]]; var pkg; for (var i = 0; i < packages.length; i++) { @@ -68,7 +68,7 @@ editor = { sources: {}, get_sources: function () { - $.getJSON('/api/sources/', function (sources) { + $.getJSON('/api/sources/?sparse=1', function (sources) { var layers = {}; var source; for (var i = 0; i < sources.length; i++) { @@ -85,7 +85,7 @@ editor = { _level: null, level_feature_layers: {}, get_levels: function () { - $.getJSON('/api/levels/?ordering=-altitude', function (levels) { + $.getJSON('/api/levels/?sparse=1&ordering=-altitude', function (levels) { L.LevelControl = L.Control.extend({ options: { position: 'bottomright' @@ -184,7 +184,7 @@ editor = { features: {}, get_features: function () { - $.getJSON('/api/features/', function(features) { + $.getJSON('/api/features/?sparse=1', function(features) { var feature_type; for (var level in editor.levels) { for (var j = 0; j < editor.feature_types_order.length; j++) { diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 3f76b160..ae25b478 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -8,16 +8,15 @@ from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet -from c3nav.mapdata.cache import AccessCachedViewSetMixin, CachedViewSetMixin from c3nav.mapdata.models import FEATURE_TYPES, Feature, Level, Package, Source from c3nav.mapdata.permissions import filter_source_queryset from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer, SourceSerializer) -class LevelViewSet(CachedViewSetMixin, ReadOnlyModelViewSet): +class LevelViewSet(ReadOnlyModelViewSet): """ - Returns a list of all levels on the map. + List and retrieve levels. """ queryset = Level.objects.all() serializer_class = LevelSerializer @@ -28,9 +27,9 @@ class LevelViewSet(CachedViewSetMixin, ReadOnlyModelViewSet): search_fields = ('name',) -class PackageViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet): +class PackageViewSet(ReadOnlyModelViewSet): """ - Returns a list of all packages the map consists of. + Retrieve packages the map consists of. """ queryset = Package.objects.all() serializer_class = PackageSerializer @@ -41,10 +40,9 @@ class PackageViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet): search_fields = ('name',) -class SourceViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet): +class SourceViewSet(ReadOnlyModelViewSet): """ - Returns a list of source images (to use as a drafts). - Call /sources/{name}/image to get the image. + List and retrieve source images (to use as a drafts). """ queryset = Source.objects.all() serializer_class = SourceSerializer @@ -69,9 +67,8 @@ class SourceViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet): class FeatureTypeViewSet(ViewSet): """ - Get Feature types + List and retrieve feature types """ - def list(self, request): serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request}) return Response(serializer.data) @@ -85,7 +82,7 @@ class FeatureTypeViewSet(ViewSet): class FeatureViewSet(ReadOnlyModelViewSet): """ - Get all Map Features + List and retrieve map features you have access to """ queryset = Feature.objects.all() serializer_class = FeatureSerializer diff --git a/src/c3nav/mapdata/models/feature.py b/src/c3nav/mapdata/models/feature.py index 817f0153..59663f97 100644 --- a/src/c3nav/mapdata/models/feature.py +++ b/src/c3nav/mapdata/models/feature.py @@ -16,6 +16,10 @@ class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'g super().__init__() FEATURE_TYPES[self.name] = self + @property + def pk(self): + return self.name + @property def title_en(self): language = get_language() diff --git a/src/c3nav/mapdata/serializers.py b/src/c3nav/mapdata/serializers.py index 1edf8729..1dddaef6 100644 --- a/src/c3nav/mapdata/serializers.py +++ b/src/c3nav/mapdata/serializers.py @@ -1,9 +1,13 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError +from rest_framework.reverse import reverse from shapely.geometry import mapping, shape +from c3nav.api.serializers import RecursiveSerializerMixin +from c3nav.editor.hosters import get_hoster_for_package from c3nav.mapdata.models import Feature, Level, Package, Source +from c3nav.mapdata.models.feature import FEATURE_TYPES from c3nav.mapdata.utils import sort_geojson @@ -27,37 +31,77 @@ class GeometryField(serializers.DictField): raise ValidationError(_('Invalid GeoJSON.')) -class LevelSerializer(serializers.ModelSerializer): - class Meta: - model = Level - fields = ('name', 'altitude', 'package') +class PackageSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): + hoster = serializers.SerializerMethodField() + depends = serializers.SerializerMethodField() - -class PackageSerializer(serializers.ModelSerializer): class Meta: model = Package - fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public') - readonly_fields = ('commit_id',) + fields = ('name', 'url', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster') + sparse_exclude = ('depends', 'hoster') + extra_kwargs = { + 'url': {'view_name': 'api:package-detail'} + } + + def get_depends(self, obj): + return self.recursive_value(PackageSerializer, obj.depends, many=True) + + def get_hoster(self, obj): + from c3nav.editor.serializers import HosterSerializer + return self.recursive_value(HosterSerializer, get_hoster_for_package(obj)) -class SourceSerializer(serializers.ModelSerializer): +class LevelSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): + package = PackageSerializer(context={'sparse': True}) + + class Meta: + model = Level + fields = ('name', 'url', 'altitude', 'package') + sparse_exclude = ('package',) + extra_kwargs = { + 'url': {'view_name': 'api:level-detail'} + } + + +class SourceSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): + image_url = serializers.SerializerMethodField() + package = PackageSerializer(context={'sparse': True}) + class Meta: model = Source - fields = ('name', 'package', 'bounds') + fields = ('name', 'url', 'image_url', 'package', 'bounds') + sparse_exclude = ('package', ) + extra_kwargs = { + 'url': {'view_name': 'api:source-detail'} + } + + def get_image_url(self, obj): + return reverse('api:source-image', args=(obj.name, ), request=self.context.get('request')) class FeatureTypeSerializer(serializers.Serializer): name = serializers.CharField() + url = serializers.HyperlinkedIdentityField(view_name='api:featuretype-detail') title = serializers.CharField() title_plural = serializers.CharField() geomtype = serializers.CharField() color = serializers.CharField() -class FeatureSerializer(serializers.ModelSerializer): +class FeatureSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): titles = serializers.JSONField() + feature_type = serializers.SerializerMethodField() + level = LevelSerializer() + package = PackageSerializer() geometry = GeometryField() class Meta: model = Feature - fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry') + fields = ('name', 'url', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry') + sparse_exclude = ('feature_type', 'level', 'package') + extra_kwargs = { + 'url': {'view_name': 'api:feature-detail'} + } + + def get_feature_type(self, obj): + return self.recursive_value(FeatureTypeSerializer, FEATURE_TYPES.get(obj.feature_type)) diff --git a/src/c3nav/urls.py b/src/c3nav/urls.py index 48bea29f..32e52222 100644 --- a/src/c3nav/urls.py +++ b/src/c3nav/urls.py @@ -8,6 +8,6 @@ import c3nav.editor.urls urlpatterns = [ url(r'^control/', include(c3nav.control.urls)), url(r'^editor/', include(c3nav.editor.urls)), - url(r'^api/', include(c3nav.api.urls)), + url(r'^api/', include(c3nav.api.urls, namespace='api')), url(r'^admin/', admin.site.urls), ]