зеркало из https://github.com/nextcloud/appstore.git
Merge pull request #57 from nextcloud/categories
Move categories into separate route because otherwise it duplicates a…
This commit is contained in:
Коммит
2800786415
198
docs/restapi.rst
198
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 <https://en.wikipedia.org/wiki/HTTP_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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<version>\d+\.\d+\.\d+)/apps\.json$',
|
||||
|
@ -10,4 +11,6 @@ urlpatterns = [
|
|||
url(r'^apps/(?P<pk>[a-z_]+)/?$', Apps.as_view(), name='app-delete'),
|
||||
url(r'^apps/(?P<app>[a-z_]+)/releases/(?P<version>\d+\.\d+\.\d+)/?$',
|
||||
AppReleases.as_view(), name='app-release-delete'),
|
||||
url(r'^categories.json$',
|
||||
etag(category_api_etag)(Categories.as_view()), name='categories'),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
Загрузка…
Ссылка в новой задаче