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),
]