reorganize API
This commit is contained in:
parent
a19f3c4de0
commit
7ea4a1bd21
10 changed files with 142 additions and 35 deletions
46
src/c3nav/api/serializers.py
Normal file
46
src/c3nav/api/serializers.py
Normal file
|
@ -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
|
|
@ -29,5 +29,6 @@
|
||||||
{% block description %}
|
{% block description %}
|
||||||
{% if breadcrumblist|length == 1 %}
|
{% if breadcrumblist|length == 1 %}
|
||||||
<p>Welcome to the c3nav RESTful API.</p>
|
<p>Welcome to the c3nav RESTful API.</p>
|
||||||
|
<p><small><em>Add <code>?sparse=1</code> to any request to flatten its result.</em></small></p>
|
||||||
{% else %}{{ description }}{% endif %}
|
{% else %}{{ description }}{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -16,12 +16,12 @@ from c3nav.mapdata.models.package import Package
|
||||||
|
|
||||||
class HosterViewSet(ViewSet):
|
class HosterViewSet(ViewSet):
|
||||||
"""
|
"""
|
||||||
Get Package Hosters
|
Retrieve and interact with package hosters
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, pk=None):
|
def retrieve(self, request, pk=None):
|
||||||
if pk not in hosters:
|
if pk not in hosters:
|
||||||
raise Http404
|
raise Http404
|
||||||
serializer = HosterSerializer(hosters[pk])
|
serializer = HosterSerializer(hosters[pk], context={'request': request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=['get'])
|
@detail_route(methods=['get'])
|
||||||
|
@ -90,7 +90,7 @@ class HosterViewSet(ViewSet):
|
||||||
|
|
||||||
class SubmitTaskViewSet(ViewSet):
|
class SubmitTaskViewSet(ViewSet):
|
||||||
"""
|
"""
|
||||||
Get Submit Tasks
|
Get hoster submit tasks
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, pk=None):
|
def retrieve(self, request, pk=None):
|
||||||
task = submit_edit_task.AsyncResult(task_id=pk)
|
task = submit_edit_task.AsyncResult(task_id=pk)
|
||||||
|
|
|
@ -15,11 +15,15 @@ class Hoster(ABC):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pk(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
def get_packages(self):
|
def get_packages(self):
|
||||||
"""
|
"""
|
||||||
Get a Queryset of all packages that can be handled by this hoster
|
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):
|
def _get_callback_uri(self, request):
|
||||||
uri = request.build_absolute_uri(reverse('editor.oauth.callback', kwargs={'hoster': self.name}))
|
uri = request.build_absolute_uri(reverse('editor.oauth.callback', kwargs={'hoster': self.name}))
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
|
||||||
class HosterSerializer(serializers.Serializer):
|
class HosterSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField()
|
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()
|
base_url = serializers.CharField()
|
||||||
packages = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_packages(self, obj):
|
def get_state_url(self, obj):
|
||||||
return tuple(obj.get_packages().values_list('name', flat=True))
|
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):
|
class TaskSerializer(serializers.Serializer):
|
||||||
id = serializers.CharField()
|
id = serializers.CharField()
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail', lookup_field='id', lookup_url_kwarg='pk')
|
||||||
started = serializers.SerializerMethodField()
|
started = serializers.SerializerMethodField()
|
||||||
done = serializers.SerializerMethodField()
|
done = serializers.SerializerMethodField()
|
||||||
success = serializers.SerializerMethodField()
|
success = serializers.SerializerMethodField()
|
||||||
|
|
|
@ -51,7 +51,7 @@ editor = {
|
||||||
|
|
||||||
packages: {},
|
packages: {},
|
||||||
get_packages: function () {
|
get_packages: function () {
|
||||||
$.getJSON('/api/packages/', function (packages) {
|
$.getJSON('/api/packages/?sparse=1', function (packages) {
|
||||||
var bounds = [[0, 0], [0, 0]];
|
var bounds = [[0, 0], [0, 0]];
|
||||||
var pkg;
|
var pkg;
|
||||||
for (var i = 0; i < packages.length; i++) {
|
for (var i = 0; i < packages.length; i++) {
|
||||||
|
@ -68,7 +68,7 @@ editor = {
|
||||||
|
|
||||||
sources: {},
|
sources: {},
|
||||||
get_sources: function () {
|
get_sources: function () {
|
||||||
$.getJSON('/api/sources/', function (sources) {
|
$.getJSON('/api/sources/?sparse=1', function (sources) {
|
||||||
var layers = {};
|
var layers = {};
|
||||||
var source;
|
var source;
|
||||||
for (var i = 0; i < sources.length; i++) {
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
@ -85,7 +85,7 @@ editor = {
|
||||||
_level: null,
|
_level: null,
|
||||||
level_feature_layers: {},
|
level_feature_layers: {},
|
||||||
get_levels: function () {
|
get_levels: function () {
|
||||||
$.getJSON('/api/levels/?ordering=-altitude', function (levels) {
|
$.getJSON('/api/levels/?sparse=1&ordering=-altitude', function (levels) {
|
||||||
L.LevelControl = L.Control.extend({
|
L.LevelControl = L.Control.extend({
|
||||||
options: {
|
options: {
|
||||||
position: 'bottomright'
|
position: 'bottomright'
|
||||||
|
@ -184,7 +184,7 @@ editor = {
|
||||||
|
|
||||||
features: {},
|
features: {},
|
||||||
get_features: function () {
|
get_features: function () {
|
||||||
$.getJSON('/api/features/', function(features) {
|
$.getJSON('/api/features/?sparse=1', function(features) {
|
||||||
var feature_type;
|
var feature_type;
|
||||||
for (var level in editor.levels) {
|
for (var level in editor.levels) {
|
||||||
for (var j = 0; j < editor.feature_types_order.length; j++) {
|
for (var j = 0; j < editor.feature_types_order.length; j++) {
|
||||||
|
|
|
@ -8,16 +8,15 @@ from rest_framework.decorators import detail_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
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.models import FEATURE_TYPES, Feature, Level, Package, Source
|
||||||
from c3nav.mapdata.permissions import filter_source_queryset
|
from c3nav.mapdata.permissions import filter_source_queryset
|
||||||
from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer,
|
from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer,
|
||||||
SourceSerializer)
|
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()
|
queryset = Level.objects.all()
|
||||||
serializer_class = LevelSerializer
|
serializer_class = LevelSerializer
|
||||||
|
@ -28,9 +27,9 @@ class LevelViewSet(CachedViewSetMixin, ReadOnlyModelViewSet):
|
||||||
search_fields = ('name',)
|
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()
|
queryset = Package.objects.all()
|
||||||
serializer_class = PackageSerializer
|
serializer_class = PackageSerializer
|
||||||
|
@ -41,10 +40,9 @@ class PackageViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet):
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
class SourceViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet):
|
class SourceViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns a list of source images (to use as a drafts).
|
List and retrieve source images (to use as a drafts).
|
||||||
Call /sources/{name}/image to get the image.
|
|
||||||
"""
|
"""
|
||||||
queryset = Source.objects.all()
|
queryset = Source.objects.all()
|
||||||
serializer_class = SourceSerializer
|
serializer_class = SourceSerializer
|
||||||
|
@ -69,9 +67,8 @@ class SourceViewSet(AccessCachedViewSetMixin, ReadOnlyModelViewSet):
|
||||||
|
|
||||||
class FeatureTypeViewSet(ViewSet):
|
class FeatureTypeViewSet(ViewSet):
|
||||||
"""
|
"""
|
||||||
Get Feature types
|
List and retrieve feature types
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request})
|
serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
@ -85,7 +82,7 @@ class FeatureTypeViewSet(ViewSet):
|
||||||
|
|
||||||
class FeatureViewSet(ReadOnlyModelViewSet):
|
class FeatureViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Get all Map Features
|
List and retrieve map features you have access to
|
||||||
"""
|
"""
|
||||||
queryset = Feature.objects.all()
|
queryset = Feature.objects.all()
|
||||||
serializer_class = FeatureSerializer
|
serializer_class = FeatureSerializer
|
||||||
|
|
|
@ -16,6 +16,10 @@ class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'g
|
||||||
super().__init__()
|
super().__init__()
|
||||||
FEATURE_TYPES[self.name] = self
|
FEATURE_TYPES[self.name] = self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pk(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title_en(self):
|
def title_en(self):
|
||||||
language = get_language()
|
language = get_language()
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
from shapely.geometry import mapping, shape
|
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 import Feature, Level, Package, Source
|
||||||
|
from c3nav.mapdata.models.feature import FEATURE_TYPES
|
||||||
from c3nav.mapdata.utils import sort_geojson
|
from c3nav.mapdata.utils import sort_geojson
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,37 +31,77 @@ class GeometryField(serializers.DictField):
|
||||||
raise ValidationError(_('Invalid GeoJSON.'))
|
raise ValidationError(_('Invalid GeoJSON.'))
|
||||||
|
|
||||||
|
|
||||||
class LevelSerializer(serializers.ModelSerializer):
|
class PackageSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
|
||||||
class Meta:
|
hoster = serializers.SerializerMethodField()
|
||||||
model = Level
|
depends = serializers.SerializerMethodField()
|
||||||
fields = ('name', 'altitude', 'package')
|
|
||||||
|
|
||||||
|
|
||||||
class PackageSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Package
|
model = Package
|
||||||
fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public')
|
fields = ('name', 'url', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster')
|
||||||
readonly_fields = ('commit_id',)
|
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:
|
class Meta:
|
||||||
model = Source
|
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):
|
class FeatureTypeSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='api:featuretype-detail')
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
title_plural = serializers.CharField()
|
title_plural = serializers.CharField()
|
||||||
geomtype = serializers.CharField()
|
geomtype = serializers.CharField()
|
||||||
color = serializers.CharField()
|
color = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class FeatureSerializer(serializers.ModelSerializer):
|
class FeatureSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
|
||||||
titles = serializers.JSONField()
|
titles = serializers.JSONField()
|
||||||
|
feature_type = serializers.SerializerMethodField()
|
||||||
|
level = LevelSerializer()
|
||||||
|
package = PackageSerializer()
|
||||||
geometry = GeometryField()
|
geometry = GeometryField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Feature
|
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))
|
||||||
|
|
|
@ -8,6 +8,6 @@ import c3nav.editor.urls
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^control/', include(c3nav.control.urls)),
|
url(r'^control/', include(c3nav.control.urls)),
|
||||||
url(r'^editor/', include(c3nav.editor.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),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue