diff --git a/src/c3nav/api/__init__.py b/src/c3nav/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/api/permissions.py b/src/c3nav/api/permissions.py new file mode 100644 index 00000000..164a9937 --- /dev/null +++ b/src/c3nav/api/permissions.py @@ -0,0 +1,28 @@ +from rest_framework.permissions import BasePermission +from rest_framework.exceptions import PermissionDenied +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from ..mapdata.models import Source + + +def get_unlocked_packages(request): + return set(settings.PUBLIC_PACKAGES) | set(request.session.get('unlocked_packages', ())) + + +def can_access_package(request, package): + print(package.name == 'de.c3nav.33c3.base') + return package.name == 'de.c3nav.33c3.base' + return settings.DEBUG or package.name in get_unlocked_packages(request) + + +def filter_source_queryset(request, queryset): + return queryset if settings.DEBUG else queryset.filter(package__name__in=get_unlocked_packages(request)) + + +class LockedMapFeatures(BasePermission): + def has_object_permission(self, request, view, obj): + if isinstance(obj, Source): + if not can_access_package(request, obj.package): + raise PermissionDenied(_('This Source belongs to a package you don\'t have access to.')) + return True diff --git a/src/c3nav/api/serializers.py b/src/c3nav/api/serializers.py new file mode 100644 index 00000000..26031f2f --- /dev/null +++ b/src/c3nav/api/serializers.py @@ -0,0 +1,29 @@ +from rest_framework.serializers import ModelSerializer + +from ..mapdata.models import Level, Package, Source + + +class BoundsMixin: + def to_representation(self, obj): + result = super().to_representation(obj) + if obj.bottom is not None: + result['bounds'] = ((obj.bottom, obj.left), (obj.top, obj.right)) + return result + + +class LevelSerializer(ModelSerializer): + class Meta: + model = Level + fields = ('name', 'altitude', 'package') + + +class PackageSerializer(BoundsMixin, ModelSerializer): + class Meta: + model = Package + fields = ('name', 'depends') + + +class SourceSerializer(BoundsMixin, ModelSerializer): + class Meta: + model = Source + fields = ('name', 'package') diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py new file mode 100644 index 00000000..9fb9f26b --- /dev/null +++ b/src/c3nav/api/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import include, url + +from rest_framework.routers import DefaultRouter + +from .views import map as map_views + +router = DefaultRouter() +router.register(r'map/levels', map_views.LevelViewSet) +router.register(r'map/packages', map_views.PackageViewSet) +router.register(r'map/sources', map_views.SourceViewSet) + + +urlpatterns = [ + url(r'^(?Pv\d+)/', include(router.urls, namespace='v1')), +] diff --git a/src/c3nav/api/views/__init__.py b/src/c3nav/api/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/api/views/map.py b/src/c3nav/api/views/map.py new file mode 100644 index 00000000..62246b73 --- /dev/null +++ b/src/c3nav/api/views/map.py @@ -0,0 +1,65 @@ +import mimetypes +import os + +from django.conf import settings +from django.core.files import File +from django.http import HttpResponse + +from rest_framework.viewsets import ReadOnlyModelViewSet +from rest_framework.decorators import detail_route + +from ...mapdata.models import Level, Package, Source +from ..serializers import LevelSerializer, PackageSerializer, SourceSerializer +from ..permissions import filter_source_queryset + + +class LevelViewSet(ReadOnlyModelViewSet): + """ + Returns a list of all levels on the map. + """ + queryset = Level.objects.all() + serializer_class = LevelSerializer + lookup_value_regex = '[^/]+' + filter_fields = ('altitude', 'package') + ordering_fields = ('altitude', 'package') + ordering = ('altitude',) + search_fields = ('name',) + + +class PackageViewSet(ReadOnlyModelViewSet): + """ + Returns a list of all packages the map consists of. + """ + queryset = Package.objects.all() + serializer_class = PackageSerializer + lookup_value_regex = '[^/]+' + filter_fields = ('name', 'depends') + ordering_fields = ('name',) + ordering = ('name',) + search_fields = ('name',) + + +class SourceViewSet(ReadOnlyModelViewSet): + """ + Returns a list of source images (to use as a drafts). + Call /sources/{name}/image to get the image. + """ + queryset = Source.objects.all() + serializer_class = SourceSerializer + 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, pk=None, version=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/settings.py b/src/c3nav/settings.py index 630722f0..831437be 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -46,6 +46,7 @@ else: debug_fallback = "runserver" in sys.argv DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback) +PUBLIC_PACKAGES = [n for n in config.get('c3nav', 'public_packages', fallback='').split(',') if n] db_backend = config.get('database', 'backend', fallback='sqlite3') DATABASES = { @@ -108,6 +109,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'compressor', 'bootstrap3', + 'rest_framework', 'c3nav.mapdata', 'c3nav.editor', 'c3nav.control', @@ -136,6 +138,22 @@ USE_I18N = True USE_L10N = True USE_TZ = True +REST_FRAMEWORK = { + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', + 'ALLOWED_VERSIONS': ['v1'], + 'DEFAULT_VERSION': 'v1', + 'DEFAULT_PERMISSION_CLASSES': ( + 'c3nav.api.permissions.LockedMapFeatures', + ), + 'DEFAULT_FILTER_BACKENDS': ( + 'rest_framework.filters.DjangoFilterBackend', + 'rest_framework.filters.OrderingFilter', + 'rest_framework.filters.SearchFilter', + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 50 +} + LOCALE_PATHS = ( os.path.join(os.path.dirname(__file__), 'locale'), ) diff --git a/src/c3nav/urls.py b/src/c3nav/urls.py index f7750591..090860c5 100644 --- a/src/c3nav/urls.py +++ b/src/c3nav/urls.py @@ -1,11 +1,13 @@ from django.conf.urls import include, url from django.contrib import admin +from .api import urls as api_urls from .control import urls as control_urls from .editor import urls as editor_urls urlpatterns = [ url(r'^control/', include(control_urls)), url(r'^editor/', include(editor_urls)), + url(r'^api/', include(api_urls)), url(r'^admin/', admin.site.urls), ] diff --git a/src/requirements.txt b/src/requirements.txt index e2dbc266..b0e37dec 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,3 +2,5 @@ Django>=1.9,<1.10 django-bootstrap3>=6.2,<6.3 django-compressor==2.0 csscompressor +djangorestframework==3.4.* +django-filter==0.14.*