diff --git a/src/c3nav/api/serializers.py b/src/c3nav/api/serializers.py deleted file mode 100644 index 54640ad2..00000000 --- a/src/c3nav/api/serializers.py +++ /dev/null @@ -1,46 +0,0 @@ -from collections import Iterable - -from django.db.models.manager import BaseManager -from rest_framework import serializers - - -class RelatedNameField(serializers.DictField): - """ - give primary key - """ - def to_representation(self, obj): - if hasattr(obj, 'name'): - return obj.name - 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] = RelatedNameField() - - 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 RelatedNameField().to_representation(obj) - return serializer(obj, context=self.sparse_context(), *args, **kwargs).data diff --git a/src/c3nav/api/templates/rest_framework/api.html b/src/c3nav/api/templates/rest_framework/api.html index cc6a6094..cef19449 100644 --- a/src/c3nav/api/templates/rest_framework/api.html +++ b/src/c3nav/api/templates/rest_framework/api.html @@ -24,11 +24,3 @@ {% block branding %} c3nav API {% endblock %} - - -{% 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/api/urls.py b/src/c3nav/api/urls.py index a52f345d..51f26eb7 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -1,9 +1,16 @@ -from rest_framework.routers import DefaultRouter +import re +from collections import OrderedDict + +from compressor.utils.decorators import cached_property +from django.conf.urls import include, url +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework.routers import SimpleRouter from c3nav.editor.api import HosterViewSet, SubmitTaskViewSet from c3nav.mapdata.api import FeatureTypeViewSet, FeatureViewSet, LevelViewSet, PackageViewSet, SourceViewSet -router = DefaultRouter() +router = SimpleRouter() router.register(r'levels', LevelViewSet) router.register(r'packages', PackageViewSet) router.register(r'sources', SourceViewSet) @@ -12,4 +19,33 @@ router.register(r'features', FeatureViewSet) router.register(r'hosters', HosterViewSet, base_name='hoster') router.register(r'submittasks', SubmitTaskViewSet, base_name='submittask') -urlpatterns = router.urls + +class APIRoot(GenericAPIView): + """ + Welcome to the c3nav RESTful API. + """ + + def _format_pattern(self, pattern): + return re.sub(r'\(\?P<([^>]*[^>_])_?>[^)]+\)', r'{\1}', pattern)[1:-1] + + @cached_property + def urls(self): + urls = OrderedDict() + for urlpattern in router.urls: + name = urlpattern.name + url = self._format_pattern(urlpattern.regex.pattern) + base = url.split('/', 1)[0] + if '-' in name: + urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url + else: + urls[name] = url + return urls + + def get(self, request): + return Response(self.urls) + + +urlpatterns = [ + url(r'^$', APIRoot.as_view()), + url(r'', include(router.urls)), +] diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index eeecc543..51597d83 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -20,18 +20,18 @@ class HosterViewSet(ViewSet): """ lookup_field = 'name' - def retrieve(self, request, pk=None): - if pk not in hosters: + def retrieve(self, request, name=None): + if name not in hosters: raise Http404 - serializer = HosterSerializer(hosters[pk], context={'request': request}) + serializer = HosterSerializer(hosters[name], context={'request': request}) return Response(serializer.data) @detail_route(methods=['get']) - def state(self, request, pk=None): - if pk not in hosters: + def state(self, request, name=None): + if name not in hosters: raise Http404 - hoster = hosters[pk] + hoster = hosters[name] state = hoster.get_state(request) error = hoster.get_error(request) if state == 'logged_out' else None @@ -41,18 +41,18 @@ class HosterViewSet(ViewSet): ))) @detail_route(methods=['post']) - def auth_uri(self, request, pk=None): - if pk not in hosters: + def auth_uri(self, request, name=None): + if name not in hosters: raise Http404 return Response({ - 'auth_uri': hosters[pk].get_auth_uri(request) + 'auth_uri': hosters[name].get_auth_uri(request) }) @detail_route(methods=['post']) - def submit(self, request, pk=None): - if pk not in hosters: + def submit(self, request, name=None): + if name not in hosters: raise Http404 - hoster = hosters[pk] + hoster = hosters[name] if 'data' not in request.POST: raise ValidationError('Missing POST parameter: data') @@ -94,10 +94,10 @@ class SubmitTaskViewSet(ViewSet): """ Get hoster submit tasks """ - lookup_field = 'id' + lookup_field = 'id_' - def retrieve(self, request, pk=None): - task = submit_edit_task.AsyncResult(task_id=pk) + def retrieve(self, request, id_=None): + task = submit_edit_task.AsyncResult(task_id=id_) try: task.ready() except: diff --git a/src/c3nav/editor/serializers.py b/src/c3nav/editor/serializers.py index b9ed3241..cea1b4c9 100644 --- a/src/c3nav/editor/serializers.py +++ b/src/c3nav/editor/serializers.py @@ -1,28 +1,13 @@ 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', lookup_field='name') - state_url = serializers.SerializerMethodField() - auth_uri_url = serializers.SerializerMethodField() - submit_url = serializers.SerializerMethodField() base_url = serializers.CharField() - 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') started = serializers.SerializerMethodField() done = serializers.SerializerMethodField() success = serializers.SerializerMethodField() diff --git a/src/c3nav/mapdata/serializers.py b/src/c3nav/mapdata/serializers.py index 90613f25..507c797a 100644 --- a/src/c3nav/mapdata/serializers.py +++ b/src/c3nav/mapdata/serializers.py @@ -1,13 +1,10 @@ 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 format_geojson @@ -31,78 +28,49 @@ class GeometryField(serializers.DictField): raise ValidationError(_('Invalid GeoJSON.')) -class PackageSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): +class PackageSerializer(serializers.ModelSerializer): hoster = serializers.SerializerMethodField() - depends = serializers.SerializerMethodField() + depends = serializers.SlugRelatedField(slug_field='name', many=True, read_only=True) class Meta: model = Package - fields = ('name', 'url', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster') - sparse_exclude = ('depends', 'hoster') - extra_kwargs = { - 'url': {'view_name': 'api:package-detail', 'lookup_field': 'name'} - } + fields = ('name', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster') 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)) + return get_hoster_for_package(obj).name -class LevelSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): - package = PackageSerializer(context={'sparse': True}) +class LevelSerializer(serializers.ModelSerializer): + package = serializers.SlugRelatedField(slug_field='name', read_only=True) class Meta: model = Level - fields = ('name', 'url', 'altitude', 'package') - sparse_exclude = ('package',) - extra_kwargs = { - 'url': {'view_name': 'api:level-detail', 'lookup_field': 'name'} - } + fields = ('name', 'altitude', 'package') -class SourceSerializer(RecursiveSerializerMixin, serializers.ModelSerializer): - image_url = serializers.SerializerMethodField() - package = PackageSerializer(context={'sparse': True}) +class SourceSerializer(serializers.ModelSerializer): + package = serializers.SlugRelatedField(slug_field='name', read_only=True) class Meta: model = Source - fields = ('name', 'url', 'image_url', 'package', 'bounds') - sparse_exclude = ('package', ) - extra_kwargs = { - 'url': {'view_name': 'api:source-detail', 'lookup_field': 'name'} - } - - def get_image_url(self, obj): - return reverse('api:source-image', args=(obj.name, ), request=self.context.get('request')) + fields = ('name', 'package', 'bounds') 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(RecursiveSerializerMixin, serializers.ModelSerializer): +class FeatureSerializer(serializers.ModelSerializer): titles = serializers.JSONField() - feature_type = serializers.SerializerMethodField() - level = LevelSerializer() - package = PackageSerializer() geometry = GeometryField() class Meta: model = Feature - fields = ('name', 'url', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry') - sparse_exclude = ('feature_type', 'level', 'package') - extra_kwargs = { - 'lookup_field': 'name', - 'url': {'view_name': 'api:feature-detail', 'lookup_field': 'name'} - } - - def get_feature_type(self, obj): - return self.recursive_value(FeatureTypeSerializer, FEATURE_TYPES.get(obj.feature_type)) + fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry')