From 5083dfb807f6caf17ef58ef97f5fca8ef27f7b80 Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Sat, 25 Jun 2016 18:56:08 +0200 Subject: [PATCH] Move categories into separate route because otherwise it duplicates a lot of data and its also hard to generate the category navigation --- docs/restapi.rst | 198 ++++++++++++++----- nextcloudappstore/core/api/v1/serializers.py | 11 +- nextcloudappstore/core/api/v1/urls.py | 5 +- nextcloudappstore/core/api/v1/views.py | 29 ++- 4 files changed, 178 insertions(+), 65 deletions(-) diff --git a/docs/restapi.rst b/docs/restapi.rst index 5019768b14..84a9e417bf 100644 --- a/docs/restapi.rst +++ b/docs/restapi.rst @@ -12,6 +12,8 @@ The following API routes are present: * :ref:`api-all-releases` +* :ref:`api-all-categories` + * :ref:`api-delete-app` * :ref:`api-delete-release` @@ -45,85 +47,91 @@ This route will return all releases to display inside Nextcloud's apps admin are { "id": "news", "categories": [ - { - "id": "tools", - "translations": { - "en": { - "name": "Tools" - }, - "de": { - "name": "Werkzeuge" - }, - "fr": { - "name": "Outil" - } - } - } + "multimedia" ], - "recommendations": 100, - "featured": false, - "userDocs": "http://127.0.0.1:8000/user", - "adminDocs": "http://127.0.0.1:8000/admin", - "developerDocs": "http://127.0.0.1:8000/dev", - "issueTracker": "http://127.0.0.1:8000/issue", - "website": "http://127.0.0.1:8000/", - "created": "2016-06-09T17:56:05.076980Z", - "lastModified": "2016-06-09T17:56:19.099038Z", + "userDocs": "https://github.com/owncloud/news/wiki#user-documentation", + "adminDocs": "https://github.com/owncloud/news#readme", + "developerDocs": "https://github.com/owncloud/news/wiki#developer-documentation", + "issueTracker": "https://github.com/owncloud/news/issues", + "website": "https://github.com/owncloud/news", + "created": "2016-06-25T16:08:56.794719Z", + "lastModified": "2016-06-25T16:49:25.326855Z", "releases": [ { - "version": "1.9.0", - "checksum": "65e613318107bceb131af5cf8b71e773b79e1a9476506f502c8e2017b52aba15", + "version": "8.8.0", "phpExtensions": [ + { + "id": "SimpleXML", + "versionSpec": "*" + }, + { + "id": "curl", + "versionSpec": "*" + }, + { + "id": "iconv", + "versionSpec": "*" + }, { "id": "libxml", - "versionSpec": ">=3.0.0 <5.0.0" + "versionSpec": ">=2.7.8" } ], "databases": [ + { + "id": "mysql", + "versionSpec": ">=5.5.0" + }, + { + "id": "pgsql", + "versionSpec": ">=9.4.0" + }, { "id": "sqlite", - "name": "Sqlite", "versionSpec": "*" } ], "shellCommands": [ "grep" ], - "phpVersionSpec": "<7.0.0", - "platformVersionSpec": ">=9.0.0", + "phpVersionSpec": ">=5.6.0", + "platformVersionSpec": ">=9.0.0 <9.2.0", "minIntSize": 64, - "download": "https://127.0.0.1:8000/download", - "created": "2016-06-09T17:57:00.587076Z", - "lastModified": "2016-06-09T17:57:00.587238Z" + "download": "https://github.com/owncloud/news/releases/download/8.8.0/news.tar.gz", + "created": "2016-06-25T16:08:56.796646Z", + "licenses": [ + "agpl" + ], + "lastModified": "2016-06-25T16:49:25.319425Z", + "checksum": "909377e1a695bbaa415c10ae087ae1cc48e88066d20a5a7a8beed149e9fad3d5" } ], - "licenses": [ - { - "id": "agpl", - "name": "AGPLv3+" - } - ], "screenshots": [ { - "url": "http://feeds2.feedburner.com/blogerator" + "url": "https://example.com/news.jpg" } ], "translations": { "en": { "name": "News", - "description": "Read News" - }, - "de": { - "name": "Neuigkeiten", - "description": "Nachrichten lesen" + "description": "An RSS/Atom feed reader" } - } + }, + "recommendations": 0, + "featured": false } ] + translations Translated fields are stored inside a translations object. They can have any size, depending on if there is a translation. If a required language is not found, you should fall back to English. +screenshots + Guaranteed to be HTTPS + +download + Download archive location, guaranteed to be HTTPS + versionSpec Required versions (minimum and maximum versions) are transformed to semantic version specs. If a field is a \*, this means that there is no version requirement. The following permutations can occur: @@ -141,6 +149,106 @@ recommendations featured Simple boolean flag which will be presented to the user as "hey take a look at this app". Does not imply that it has been reviewed or we recommend it officially +categories + The string value is the category's id attribute, see :ref:`api-all-categories` + +.. _api-all-categories: + +Get All Categories +~~~~~~~~~~~~~~~~~~ +This route will return all categories and their translations. + +* **Url**: GET /api/v1/categories.json + +* **Authentication**: None + +* **Caching**: `ETag `_ + +* **Example CURL request**:: + + curl http://localhost:8000/api/v1/categories.json -H 'If-None-Match: "4-2016-06-11 10:37:24+00:00"' + +* **Returns**: application/json + +.. code-block:: json + + [ + { + "id": "games", + "translations": { + "en": { + "name": "Games", + "description": "" + }, + "de": { + "name": "Spiele", + "description": "" + }, + "fr": { + "name": "Jeux", + "description": "" + } + } + }, + { + "id": "multimedia", + "translations": { + "en": { + "name": "Multimedia", + "description": "" + }, + "de": { + "name": "Multimedia", + "description": "" + }, + "fr": { + "name": "Multimedia", + "description": "" + } + } + }, + { + "id": "pim", + "translations": { + "en": { + "name": "PIM", + "description": "" + }, + "de": { + "name": "PIM", + "description": "" + }, + "fr": { + "name": "PIM", + "description": "" + } + } + }, + { + "id": "tools", + "translations": { + "en": { + "name": "Tools", + "description": "" + }, + "de": { + "name": "Werkzeuge", + "description": "" + }, + "fr": { + "name": "Outil", + "description": "" + } + } + } + ] + + +translations + Translated fields are stored inside a translations object. They can have any size, depending on if there is a translation. If a required language is not found, you should fall back to English. + + + .. _api-delete-app: Delete an App diff --git a/nextcloudappstore/core/api/v1/serializers.py b/nextcloudappstore/core/api/v1/serializers.py index 95376d7ef7..0048bf3604 100644 --- a/nextcloudappstore/core/api/v1/serializers.py +++ b/nextcloudappstore/core/api/v1/serializers.py @@ -20,23 +20,16 @@ class PhpExtensionDependencySerializer(serializers.ModelSerializer): class DatabaseDependencySerializer(serializers.ModelSerializer): id = serializers.ReadOnlyField(source='database.id') - name = serializers.ReadOnlyField(source='database.name') version_spec = SerializerMethodField() class Meta: model = DatabaseDependency - fields = ('id', 'name', 'version_spec') + fields = ('id', 'version_spec') def get_version_spec(self, obj): return obj.version_spec.replace(',', ' ') -class LicenseSerializer(serializers.ModelSerializer): - class Meta: - model = License - fields = ('id', 'name') - - class CategorySerializer(TranslatableModelSerializer): translations = TranslatedFieldsField(shared_model=Category) @@ -51,7 +44,6 @@ class AppReleaseSerializer(serializers.ModelSerializer): php_extensions = \ PhpExtensionDependencySerializer(many=True, read_only=True, source='phpextensiondependencies') - licenses = LicenseSerializer(many=True, read_only=True) php_version_spec = SerializerMethodField() platform_version_spec = SerializerMethodField() @@ -77,7 +69,6 @@ class ScreenshotSerializer(serializers.ModelSerializer): class AppSerializer(serializers.ModelSerializer): - categories = CategorySerializer(many=True, read_only=True) releases = AppReleaseSerializer(many=True, read_only=True) screenshots = ScreenshotSerializer(many=True, read_only=True) translations = TranslatedFieldsField(shared_model=App) diff --git a/nextcloudappstore/core/api/v1/urls.py b/nextcloudappstore/core/api/v1/urls.py index 9491b4a242..6f35afe1f6 100644 --- a/nextcloudappstore/core/api/v1/urls.py +++ b/nextcloudappstore/core/api/v1/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import url from django.views.decorators.http import etag -from nextcloudappstore.core.api.v1.views import Apps, AppReleases, app_api_etag +from nextcloudappstore.core.api.v1.views import Apps, AppReleases, \ + app_api_etag, Categories, category_api_etag urlpatterns = [ url(r'^platform/(?P\d+\.\d+\.\d+)/apps\.json$', @@ -10,4 +11,6 @@ urlpatterns = [ url(r'^apps/(?P[a-z_]+)/?$', Apps.as_view(), name='app-delete'), url(r'^apps/(?P[a-z_]+)/releases/(?P\d+\.\d+\.\d+)/?$', AppReleases.as_view(), name='app-release-delete'), + url(r'^categories.json$', + etag(category_api_etag)(Categories.as_view()), name='categories'), ] diff --git a/nextcloudappstore/core/api/v1/views.py b/nextcloudappstore/core/api/v1/views.py index 14d838571e..8d65667910 100644 --- a/nextcloudappstore/core/api/v1/views.py +++ b/nextcloudappstore/core/api/v1/views.py @@ -3,15 +3,15 @@ from django.http import Http404 from nextcloudappstore.core.api.v1.release.importer import ReleaseImporter from nextcloudappstore.core.api.v1.release.provider import AppReleaseProvider from nextcloudappstore.core.api.v1.serializers import AppSerializer, \ - AppReleaseDownloadSerializer + AppReleaseDownloadSerializer, CategorySerializer from django.db.models import Max, Count -from nextcloudappstore.core.models import App, AppRelease +from nextcloudappstore.core.models import App, AppRelease, Category from nextcloudappstore.core.permissions import UpdateDeletePermission from nextcloudappstore.core.throttling import PostThrottle from pymple import Container from rest_framework import authentication # type: ignore from rest_framework.generics import DestroyAPIView, \ - get_object_or_404 # type: ignore + get_object_or_404, ListAPIView # type: ignore from rest_framework.permissions import IsAuthenticated # type: ignore from rest_framework.response import Response # type: ignore from semantic_version import Version, Spec @@ -39,6 +39,21 @@ def app_api_etag(request, version): return '%s-%s' % (count, release_modified) +def category_api_etag(request): + category_aggr = Category.objects.aggregate(count=Count('*'), + modified=Max('last_modified')) + category_modified = category_aggr['modified'] + if category_modified is None: + return None + else: + return '%s-%s' % (category_aggr['count'], category_modified) + + +class Categories(ListAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + + class Apps(DestroyAPIView): authentication_classes = (authentication.BasicAuthentication,) permission_classes = (UpdateDeletePermission,) @@ -46,13 +61,9 @@ class Apps(DestroyAPIView): queryset = App.objects.all() def get(self, request, *args, **kwargs): - apps = App.objects.prefetch_related('translations', - 'categories__translations', - 'categories', 'releases', - 'screenshots', - 'releases__databases', + apps = App.objects.prefetch_related('translations', 'screenshots', + 'releases', 'releases__databases', 'releases__php_extensions').all() - platform_version = Version(self.kwargs['version']) def app_filter(app):