diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index ab239e23f3..2c47b338ad 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -6,7 +6,8 @@ from django.http import JsonResponse from django.utils.translation import gettext_lazy as _ from django_q.models import OrmQ -from rest_framework import permissions +from drf_spectacular.utils import OpenApiResponse, extend_schema +from rest_framework import permissions, serializers from rest_framework.response import Response from rest_framework.serializers import ValidationError from rest_framework.views import APIView @@ -21,8 +22,9 @@ from plugin.serializers import MetadataSerializer from users.models import ApiToken from .email import is_email_configured -from .mixins import RetrieveUpdateAPI +from .mixins import ListAPI, RetrieveUpdateAPI from .status import check_system_health, is_worker_running +from .version import inventreeApiText from .views import AjaxView @@ -57,6 +59,34 @@ class VersionView(APIView): }) +class VersionSerializer(serializers.Serializer): + """Serializer for a single version.""" + version = serializers.CharField() + date = serializers.CharField() + gh = serializers.CharField() + text = serializers.CharField() + latest = serializers.BooleanField() + + class Meta: + """Meta class for VersionSerializer.""" + fields = ['version', 'date', 'gh', 'text', 'latest'] + + +class VersionApiSerializer(serializers.Serializer): + """Serializer for the version api endpoint.""" + VersionSerializer(many=True) + + +class VersionTextView(ListAPI): + """Simple JSON endpoint for InvenTree version text.""" + permission_classes = [permissions.IsAdminUser] + + @extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)}) + def list(self, request, *args, **kwargs): + """Return information about the InvenTree server.""" + return JsonResponse(inventreeApiText()) + + class InfoView(AjaxView): """Simple JSON endpoint for InvenTree information. diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 5797d31e3c..24e939b7e5 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -3,10 +3,9 @@ # InvenTree API version INVENTREE_API_VERSION = 148 +"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" -""" -Increment this API version number whenever there is a significant change to the API that any clients need to know about - +INVENTREE_API_TEXT = """ v148 -> 2023-11-06 : https://github.com/inventree/InvenTree/pull/5872 - Allow "quantity" to be specified when installing an item into another item @@ -115,7 +114,7 @@ v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854 v116 -> 2023-05-18 : https://github.com/inventree/InvenTree/pull/4823 - Updates to part parameter implementation, to use physical units -v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846 +v115 -> 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846 - Adds ability to partially scrap a build output v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825 diff --git a/InvenTree/InvenTree/test_api_version.py b/InvenTree/InvenTree/test_api_version.py new file mode 100644 index 0000000000..d6a2c38fb6 --- /dev/null +++ b/InvenTree/InvenTree/test_api_version.py @@ -0,0 +1,41 @@ +"""Tests for api_version.""" + + +from django.urls import reverse + +from InvenTree.api_version import INVENTREE_API_VERSION +from InvenTree.unit_test import InvenTreeAPITestCase +from InvenTree.version import inventreeApiText, parse_version_text + + +class ApiVersionTests(InvenTreeAPITestCase): + """Tests for api_version functions and APIs.""" + + def test_api(self): + """Test that the API text is correct.""" + url = reverse('api-version-text') + response = self.client.get(url, format='json') + data = response.json() + + self.assertEqual(len(data), 10) + + def test_inventree_api_text(self): + """Test that the inventreeApiText function works expected.""" + # Normal run + resp = inventreeApiText() + self.assertEqual(len(resp), 10) + + # More responses + resp = inventreeApiText(20) + self.assertEqual(len(resp), 20) + + # Specific version + resp = inventreeApiText(start_version=5) + self.assertEqual(list(resp)[0], 'v5') + + def test_parse_version_text(self): + """Test that api version text is correctly parsed.""" + resp = parse_version_text() + + # Check that all texts are parsed + self.assertEqual(len(resp), INVENTREE_API_VERSION - 1) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 30c6a29e5e..1208e52cb1 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -36,7 +36,8 @@ from plugin.urls import get_plugin_urls from stock.urls import stock_urls from web.urls import urlpatterns as platform_urls -from .api import APISearchView, InfoView, NotFoundView, VersionView +from .api import (APISearchView, InfoView, NotFoundView, VersionTextView, + VersionView) from .magic_login import GetSimpleLoginView from .social_auth_urls import (EmailListView, EmailPrimaryView, EmailRemoveView, EmailVerifyView, @@ -79,6 +80,7 @@ apipatterns = [ re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'), # InvenTree information endpoints + path("version-text", VersionTextView.as_view(), name="api-version-text"), # version text path('version/', VersionView.as_view(), name='api-version'), # version info path('', InfoView.as_view(), name='api-inventree-info'), # server info diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index cb64def136..1ddd7b42df 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -16,7 +16,7 @@ from django.conf import settings from dulwich.repo import NotGitRepository, Repo -from .api_version import INVENTREE_API_VERSION +from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION # InvenTree software version INVENTREE_SW_VERSION = "0.13.0 dev" @@ -142,6 +142,52 @@ def inventreeApiVersion(): return INVENTREE_API_VERSION +def parse_version_text(): + """Parse the version text to structured data.""" + patched_data = INVENTREE_API_TEXT.split("\n\n") + # Remove first newline on latest version + patched_data[0] = patched_data[0].replace("\n", "", 1) + + version_data = {} + for version in patched_data: + data = version.split("\n") + + version_split = data[0].split(' -> ') + version_detail = version_split[1].split(':', 1) if len(version_split) > 1 else ['', ] + new_data = { + "version": version_split[0].strip(), + "date": version_detail[0].strip(), + "gh": version_detail[1].strip() if len(version_detail) > 1 else None, + "text": data[1:], + "latest": False, + } + version_data[new_data["version"]] = new_data + return version_data + + +INVENTREE_API_TEXT_DATA = parse_version_text() +"""Pre-processed API version text.""" + + +def inventreeApiText(versions: int = 10, start_version: int = 0): + """Returns API version descriptors. + + Args: + versions: Number of versions to return. Default: 10 + start_version: first version to report. Defaults to return the latest {versions} versions. + """ + version_data = INVENTREE_API_TEXT_DATA + + # Define the range of versions to return + if start_version == 0: + start_version = INVENTREE_API_VERSION - versions + + return { + f"v{a}": version_data.get(f"v{a}", None) + for a in range(start_version, start_version + versions) + } + + def inventreeDjangoVersion(): """Returns the version of Django library.""" return django.get_version()