mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Generate API docs (#6319)
* Add basic task for generating apidocs * Fix SPECTACTULAR_SETTINGS - Some provided options were not correct * Update .gitignore * Fix for duplicated API path - `/api/plugins/activate` routed to PluginActivate view - Must be associated with a specific plugin ID * By default, fail if warnings are raised * Use GenericAPIView for GetAuthToken * Use GenericAPIView for RolesDetail endpoint * Refactor more endpoints to use GenericApiView * More API cleanup * Add extra type hints for exposed methods * Update RoleDetails endpoint - Specify serializer - Use RetrieveAPI class type * More type hints * Export API docs as part of CI * add more api views docs * even more docs * extend tests to api-version * simplify serializer * and more docs * fix serializer * added more API docs * clean diff * Added APISearch base * do not assume you know the user he might be anonymously creating the schema ;-) * set empty serializer where no input is needed * Use dummy model for schema generation * fix OpenAPI docs section * Update .github/workflows/qc_checks.yaml Co-authored-by: Matthias Mair <code@mjmair.com> * REmove duplicate commands * Ignore warnings in CI --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
parent
a99ba75fed
commit
af4d888b1b
27
.github/workflows/qc_checks.yaml
vendored
27
.github/workflows/qc_checks.yaml
vendored
@ -112,6 +112,33 @@ jobs:
|
|||||||
pip install linkcheckmd requests
|
pip install linkcheckmd requests
|
||||||
python -m linkcheckmd docs --recurse
|
python -m linkcheckmd docs --recurse
|
||||||
|
|
||||||
|
schema:
|
||||||
|
name: Tests - API Schema Documentation
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs: paths-filter
|
||||||
|
if: needs.paths-filter.outputs.server == 'true'
|
||||||
|
env:
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
|
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||||
|
INVENTREE_ADMIN_USER: testuser
|
||||||
|
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||||
|
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||||
|
INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345
|
||||||
|
INVENTREE_PYTHON_TEST_USERNAME: testuser
|
||||||
|
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||||
|
- name: Environment Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
apt-dependency: gettext poppler-utils
|
||||||
|
dev-install: true
|
||||||
|
update: true
|
||||||
|
- name: Export API Documentation
|
||||||
|
run: |
|
||||||
|
invoke schema --ignore-warnings
|
||||||
|
|
||||||
python:
|
python:
|
||||||
name: Tests - inventree-python
|
name: Tests - inventree-python
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -48,6 +48,7 @@ label.pdf
|
|||||||
label.png
|
label.png
|
||||||
InvenTree/my_special*
|
InvenTree/my_special*
|
||||||
_tests*.txt
|
_tests*.txt
|
||||||
|
schema.yml
|
||||||
|
|
||||||
# Local static and media file storage (only when running in development mode)
|
# Local static and media file storage (only when running in development mode)
|
||||||
inventree_media
|
inventree_media
|
||||||
@ -70,6 +71,7 @@ secret_key.txt
|
|||||||
.idea/
|
.idea/
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.bash_history
|
.bash_history
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
# https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@ -107,5 +109,8 @@ InvenTree/plugins/
|
|||||||
*.mo
|
*.mo
|
||||||
messages.ts
|
messages.ts
|
||||||
|
|
||||||
|
# Generated API schema file
|
||||||
|
api.yaml
|
||||||
|
|
||||||
# web frontend (static files)
|
# web frontend (static files)
|
||||||
InvenTree/web/static
|
InvenTree/web/static
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Main JSON interface views."""
|
"""Main JSON interface views."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@ -8,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_q.models import OrmQ
|
from django_q.models import OrmQ
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import permissions, serializers
|
from rest_framework import permissions, serializers
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@ -18,6 +21,7 @@ from InvenTree.filters import SEARCH_ORDER_FILTER
|
|||||||
from InvenTree.mixins import ListCreateAPI
|
from InvenTree.mixins import ListCreateAPI
|
||||||
from InvenTree.permissions import RolePermission
|
from InvenTree.permissions import RolePermission
|
||||||
from InvenTree.templatetags.inventree_extras import plugins_info
|
from InvenTree.templatetags.inventree_extras import plugins_info
|
||||||
|
from part.models import Part
|
||||||
from plugin.serializers import MetadataSerializer
|
from plugin.serializers import MetadataSerializer
|
||||||
from users.models import ApiToken
|
from users.models import ApiToken
|
||||||
|
|
||||||
@ -28,11 +32,41 @@ from .version import inventreeApiText
|
|||||||
from .views import AjaxView
|
from .views import AjaxView
|
||||||
|
|
||||||
|
|
||||||
|
class VersionViewSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for a single version."""
|
||||||
|
|
||||||
|
class VersionSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for server version."""
|
||||||
|
|
||||||
|
server = serializers.CharField()
|
||||||
|
api = serializers.IntegerField()
|
||||||
|
commit_hash = serializers.CharField()
|
||||||
|
commit_date = serializers.CharField()
|
||||||
|
commit_branch = serializers.CharField()
|
||||||
|
python = serializers.CharField()
|
||||||
|
django = serializers.CharField()
|
||||||
|
|
||||||
|
class LinkSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for all possible links."""
|
||||||
|
|
||||||
|
doc = serializers.URLField()
|
||||||
|
code = serializers.URLField()
|
||||||
|
credit = serializers.URLField()
|
||||||
|
app = serializers.URLField()
|
||||||
|
bug = serializers.URLField()
|
||||||
|
|
||||||
|
dev = serializers.BooleanField()
|
||||||
|
up_to_date = serializers.BooleanField()
|
||||||
|
version = VersionSerializer()
|
||||||
|
links = LinkSerializer()
|
||||||
|
|
||||||
|
|
||||||
class VersionView(APIView):
|
class VersionView(APIView):
|
||||||
"""Simple JSON endpoint for InvenTree version information."""
|
"""Simple JSON endpoint for InvenTree version information."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAdminUser]
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
@extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)})
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Return information about the InvenTree server."""
|
"""Return information about the InvenTree server."""
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
@ -81,6 +115,8 @@ class VersionApiSerializer(serializers.Serializer):
|
|||||||
class VersionTextView(ListAPI):
|
class VersionTextView(ListAPI):
|
||||||
"""Simple JSON endpoint for InvenTree version text."""
|
"""Simple JSON endpoint for InvenTree version text."""
|
||||||
|
|
||||||
|
serializer_class = VersionSerializer
|
||||||
|
|
||||||
permission_classes = [permissions.IsAdminUser]
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
@extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)})
|
@extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)})
|
||||||
@ -324,7 +360,17 @@ class AttachmentMixin:
|
|||||||
attachment.save()
|
attachment.save()
|
||||||
|
|
||||||
|
|
||||||
class APISearchView(APIView):
|
class APISearchViewSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the APISearchView."""
|
||||||
|
|
||||||
|
search = serializers.CharField()
|
||||||
|
search_regex = serializers.BooleanField(default=False, required=False)
|
||||||
|
search_whole = serializers.BooleanField(default=False, required=False)
|
||||||
|
limit = serializers.IntegerField(default=1, required=False)
|
||||||
|
offset = serializers.IntegerField(default=0, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class APISearchView(GenericAPIView):
|
||||||
"""A general-purpose 'search' API endpoint.
|
"""A general-purpose 'search' API endpoint.
|
||||||
|
|
||||||
Returns hits against a number of different models simultaneously,
|
Returns hits against a number of different models simultaneously,
|
||||||
@ -334,6 +380,7 @@ class APISearchView(APIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = APISearchViewSerializer
|
||||||
|
|
||||||
def get_result_types(self):
|
def get_result_types(self):
|
||||||
"""Construct a list of search types we can return."""
|
"""Construct a list of search types we can return."""
|
||||||
@ -446,4 +493,7 @@ class MetadataView(RetrieveUpdateAPI):
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Return MetadataSerializer instance."""
|
"""Return MetadataSerializer instance."""
|
||||||
|
# Detect if we are currently generating the OpenAPI schema
|
||||||
|
if 'spectacular' in sys.argv:
|
||||||
|
return MetadataSerializer(Part, *args, **kwargs)
|
||||||
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
|
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
|
||||||
|
@ -9,8 +9,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
import sesame.utils
|
import sesame.utils
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
import InvenTree.version
|
import InvenTree.version
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ class GetSimpleLoginSerializer(serializers.Serializer):
|
|||||||
email = serializers.CharField(label=_('Email'))
|
email = serializers.CharField(label=_('Email'))
|
||||||
|
|
||||||
|
|
||||||
class GetSimpleLoginView(APIView):
|
class GetSimpleLoginView(GenericAPIView):
|
||||||
"""View to send a simple login link."""
|
"""View to send a simple login link."""
|
||||||
|
|
||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
@ -28,6 +28,10 @@ from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
|||||||
from InvenTree.helpers_model import download_image_from_url, get_base_url
|
from InvenTree.helpers_model import download_image_from_url, get_base_url
|
||||||
|
|
||||||
|
|
||||||
|
class EmptySerializer(serializers.Serializer):
|
||||||
|
"""Empty serializer for use in testing."""
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeMoneySerializer(MoneyField):
|
class InvenTreeMoneySerializer(MoneyField):
|
||||||
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
|
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
|
||||||
|
|
||||||
|
@ -517,12 +517,15 @@ if USE_JWT:
|
|||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
'TITLE': 'InvenTree API',
|
'TITLE': 'InvenTree API',
|
||||||
'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system',
|
'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system',
|
||||||
'LICENSE': {'MIT': 'https://github.com/inventree/InvenTree/blob/master/LICENSE'},
|
'LICENSE': {
|
||||||
'EXTERNAL_DOCS': {
|
'name': 'MIT',
|
||||||
'docs': 'https://docs.inventree.org',
|
'url': 'https://github.com/inventree/InvenTree/blob/master/LICENSE',
|
||||||
'web': 'https://inventree.org',
|
|
||||||
},
|
},
|
||||||
'VERSION': inventreeApiVersion(),
|
'EXTERNAL_DOCS': {
|
||||||
|
'description': 'More information about InvenTree in the official docs',
|
||||||
|
'url': 'https://docs.inventree.org',
|
||||||
|
},
|
||||||
|
'VERSION': str(inventreeApiVersion()),
|
||||||
'SERVE_INCLUDE_SCHEMA': False,
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from allauth.account.models import EmailAddress
|
|||||||
from allauth.socialaccount import providers
|
from allauth.socialaccount import providers
|
||||||
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
|
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import NotFound
|
from rest_framework.exceptions import NotFound
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -16,7 +17,7 @@ from rest_framework.response import Response
|
|||||||
import InvenTree.sso
|
import InvenTree.sso
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
|
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
@ -112,11 +113,36 @@ for name, provider in providers.registry.provider_map.items():
|
|||||||
social_auth_urlpatterns += provider_urlpatterns
|
social_auth_urlpatterns += provider_urlpatterns
|
||||||
|
|
||||||
|
|
||||||
|
class SocialProviderListResponseSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the SocialProviderListView."""
|
||||||
|
|
||||||
|
class SocialProvider(serializers.Serializer):
|
||||||
|
"""Serializer for the SocialProviderListResponseSerializer."""
|
||||||
|
|
||||||
|
id = serializers.CharField()
|
||||||
|
name = serializers.CharField()
|
||||||
|
configured = serializers.BooleanField()
|
||||||
|
login = serializers.URLField()
|
||||||
|
connect = serializers.URLField()
|
||||||
|
display_name = serializers.CharField()
|
||||||
|
|
||||||
|
sso_enabled = serializers.BooleanField()
|
||||||
|
sso_registration = serializers.BooleanField()
|
||||||
|
mfa_required = serializers.BooleanField()
|
||||||
|
providers = SocialProvider(many=True)
|
||||||
|
registration_enabled = serializers.BooleanField()
|
||||||
|
password_forgotten_enabled = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
class SocialProviderListView(ListAPI):
|
class SocialProviderListView(ListAPI):
|
||||||
"""List of available social providers."""
|
"""List of available social providers."""
|
||||||
|
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses={200: OpenApiResponse(response=SocialProviderListResponseSerializer)}
|
||||||
|
)
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Get the list of providers."""
|
"""Get the list of providers."""
|
||||||
provider_list = []
|
provider_list = []
|
||||||
|
@ -18,6 +18,11 @@ class ApiVersionTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(data), 10)
|
self.assertEqual(len(data), 10)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('api-version'), format='json').json()
|
||||||
|
self.assertIn('version', response)
|
||||||
|
self.assertIn('dev', response)
|
||||||
|
self.assertIn('up_to_date', response)
|
||||||
|
|
||||||
def test_inventree_api_text(self):
|
def test_inventree_api_text(self):
|
||||||
"""Test that the inventreeApiText function works expected."""
|
"""Test that the inventreeApiText function works expected."""
|
||||||
# Normal run
|
# Normal run
|
||||||
|
@ -11,6 +11,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
import django_q.models
|
import django_q.models
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||||
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from error_report.models import Error
|
from error_report.models import Error
|
||||||
from rest_framework import permissions, serializers
|
from rest_framework import permissions, serializers
|
||||||
from rest_framework.exceptions import NotAcceptable, NotFound
|
from rest_framework.exceptions import NotAcceptable, NotFound
|
||||||
@ -53,7 +54,15 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||||||
permission_classes = []
|
permission_classes = []
|
||||||
model_class = common.models.WebhookEndpoint
|
model_class = common.models.WebhookEndpoint
|
||||||
run_async = False
|
run_async = False
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
description='Any data can be posted to the endpoint - everything will be passed to the WebhookEndpoint model.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
def post(self, request, endpoint, *args, **kwargs):
|
def post(self, request, endpoint, *args, **kwargs):
|
||||||
"""Process incoming webhook."""
|
"""Process incoming webhook."""
|
||||||
# get webhook definition
|
# get webhook definition
|
||||||
@ -115,6 +124,7 @@ class CurrencyExchangeView(APIView):
|
|||||||
"""API endpoint for displaying currency information."""
|
"""API endpoint for displaying currency information."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
"""Return information on available currency conversions."""
|
"""Return information on available currency conversions."""
|
||||||
@ -157,6 +167,7 @@ class CurrencyRefreshView(APIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
|
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Performing a POST request will update currency exchange rates."""
|
"""Performing a POST request will update currency exchange rates."""
|
||||||
@ -516,6 +527,7 @@ class BackgroundTaskOverview(APIView):
|
|||||||
"""Provides an overview of the background task queue status."""
|
"""Provides an overview of the background task queue status."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
"""Return information about the current status of the background task queue."""
|
"""Return information about the current status of the background task queue."""
|
||||||
|
@ -2868,7 +2868,7 @@ class NotificationMessage(models.Model):
|
|||||||
"""Return API endpoint."""
|
"""Return API endpoint."""
|
||||||
return reverse('api-notifications-list')
|
return reverse('api-notifications-list')
|
||||||
|
|
||||||
def age(self):
|
def age(self) -> int:
|
||||||
"""Age of the message in seconds."""
|
"""Age of the message in seconds."""
|
||||||
# Add timezone information if TZ is enabled (in production mode mostly)
|
# Add timezone information if TZ is enabled (in production mode mostly)
|
||||||
delta = now() - (
|
delta = now() - (
|
||||||
@ -2878,7 +2878,7 @@ class NotificationMessage(models.Model):
|
|||||||
)
|
)
|
||||||
return delta.seconds
|
return delta.seconds
|
||||||
|
|
||||||
def age_human(self):
|
def age_human(self) -> str:
|
||||||
"""Humanized age."""
|
"""Humanized age."""
|
||||||
return naturaltime(self.creation)
|
return naturaltime(self.creation)
|
||||||
|
|
||||||
|
@ -59,6 +59,8 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
units = serializers.CharField(read_only=True)
|
units = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
typ = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_choices(self, obj):
|
def get_choices(self, obj):
|
||||||
"""Returns the choices available for a given item."""
|
"""Returns the choices available for a given item."""
|
||||||
results = []
|
results = []
|
||||||
@ -195,7 +197,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
read = serializers.BooleanField()
|
read = serializers.BooleanField()
|
||||||
|
|
||||||
def get_target(self, obj):
|
def get_target(self, obj) -> dict:
|
||||||
"""Function to resolve generic object reference to target."""
|
"""Function to resolve generic object reference to target."""
|
||||||
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
|
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
|
||||||
|
|
||||||
@ -217,7 +219,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def get_source(self, obj):
|
def get_source(self, obj) -> dict:
|
||||||
"""Function to resolve generic object reference to source."""
|
"""Function to resolve generic object reference to source."""
|
||||||
return get_objectreference(obj, 'source_content_type', 'source_object_id')
|
return get_objectreference(obj, 'source_content_type', 'source_object_id')
|
||||||
|
|
||||||
|
@ -2,15 +2,24 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from rest_framework import permissions
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
|
from rest_framework import permissions, serializers
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ValidationError
|
|
||||||
from rest_framework.views import APIView
|
from InvenTree.serializers import EmptySerializer
|
||||||
|
|
||||||
from .states import StatusCode
|
from .states import StatusCode
|
||||||
|
|
||||||
|
|
||||||
class StatusView(APIView):
|
class StatusViewSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the StatusView responses."""
|
||||||
|
|
||||||
|
class_name = serializers.CharField()
|
||||||
|
values = serializers.DictField()
|
||||||
|
|
||||||
|
|
||||||
|
class StatusView(GenericAPIView):
|
||||||
"""Generic API endpoint for discovering information on 'status codes' for a particular model.
|
"""Generic API endpoint for discovering information on 'status codes' for a particular model.
|
||||||
|
|
||||||
This class should be implemented as a subclass for each type of status.
|
This class should be implemented as a subclass for each type of status.
|
||||||
@ -28,12 +37,19 @@ class StatusView(APIView):
|
|||||||
status_model = self.kwargs.get(self.MODEL_REF, None)
|
status_model = self.kwargs.get(self.MODEL_REF, None)
|
||||||
|
|
||||||
if status_model is None:
|
if status_model is None:
|
||||||
raise ValidationError(
|
raise serializers.ValidationError(
|
||||||
f"StatusView view called without '{self.MODEL_REF}' parameter"
|
f"StatusView view called without '{self.MODEL_REF}' parameter"
|
||||||
)
|
)
|
||||||
|
|
||||||
return status_model
|
return status_model
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
description='Retrieve information about a specific status code',
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(description='Status code information'),
|
||||||
|
400: OpenApiResponse(description='Invalid request'),
|
||||||
|
},
|
||||||
|
)
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Perform a GET request to learn information about status codes."""
|
"""Perform a GET request to learn information about status codes."""
|
||||||
status_class = self.get_status_model()
|
status_class = self.get_status_model()
|
||||||
@ -53,6 +69,7 @@ class AllStatusViews(StatusView):
|
|||||||
"""Endpoint for listing all defined status models."""
|
"""Endpoint for listing all defined status models."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Perform a GET request to learn information about status codes."""
|
"""Perform a GET request to learn information about status codes."""
|
||||||
|
@ -49,6 +49,7 @@ from InvenTree.mixins import (
|
|||||||
UpdateAPI,
|
UpdateAPI,
|
||||||
)
|
)
|
||||||
from InvenTree.permissions import RolePermission
|
from InvenTree.permissions import RolePermission
|
||||||
|
from InvenTree.serializers import EmptySerializer
|
||||||
from InvenTree.status_codes import (
|
from InvenTree.status_codes import (
|
||||||
BuildStatusGroups,
|
BuildStatusGroups,
|
||||||
PurchaseOrderStatusGroups,
|
PurchaseOrderStatusGroups,
|
||||||
@ -487,6 +488,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all()
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Return scheduling information for the referenced Part instance."""
|
"""Return scheduling information for the referenced Part instance."""
|
||||||
@ -687,6 +689,7 @@ class PartRequirements(RetrieveAPI):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all()
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Construct a response detailing Part requirements."""
|
"""Construct a response detailing Part requirements."""
|
||||||
@ -738,6 +741,7 @@ class PartSerialNumberDetail(RetrieveAPI):
|
|||||||
"""API endpoint for returning extra serial number information about a particular part."""
|
"""API endpoint for returning extra serial number information about a particular part."""
|
||||||
|
|
||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all()
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Return serial number information for the referenced Part instance."""
|
"""Return serial number information for the referenced Part instance."""
|
||||||
@ -1068,7 +1072,11 @@ class PartMixin:
|
|||||||
|
|
||||||
# Pass a list of "starred" parts to the current user to the serializer
|
# Pass a list of "starred" parts to the current user to the serializer
|
||||||
# We do this to reduce the number of database queries required!
|
# We do this to reduce the number of database queries required!
|
||||||
if self.starred_parts is None and self.request is not None:
|
if (
|
||||||
|
self.starred_parts is None
|
||||||
|
and self.request is not None
|
||||||
|
and hasattr(self.request.user, 'starred_parts')
|
||||||
|
):
|
||||||
self.starred_parts = [
|
self.starred_parts = [
|
||||||
star.part for star in self.request.user.starred_parts.all()
|
star.part for star in self.request.user.starred_parts.all()
|
||||||
]
|
]
|
||||||
|
@ -748,7 +748,7 @@ class Part(
|
|||||||
return stock[0].serial
|
return stock[0].serial
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self) -> str:
|
||||||
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings."""
|
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings."""
|
||||||
return part_helpers.render_part_full_name(self)
|
return part_helpers.render_part_full_name(self)
|
||||||
|
|
||||||
@ -762,7 +762,7 @@ class Part(
|
|||||||
return helpers.getMediaUrl(self.image.url)
|
return helpers.getMediaUrl(self.image.url)
|
||||||
return helpers.getBlankImage()
|
return helpers.getBlankImage()
|
||||||
|
|
||||||
def get_thumbnail_url(self):
|
def get_thumbnail_url(self) -> str:
|
||||||
"""Return the URL of the image thumbnail for this part."""
|
"""Return the URL of the image thumbnail for this part."""
|
||||||
if self.image:
|
if self.image:
|
||||||
return helpers.getMediaUrl(self.image.thumbnail.url)
|
return helpers.getMediaUrl(self.image.thumbnail.url)
|
||||||
|
@ -91,7 +91,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
if not path_detail:
|
if not path_detail:
|
||||||
self.fields.pop('path')
|
self.fields.pop('path')
|
||||||
|
|
||||||
def get_starred(self, category):
|
def get_starred(self, category) -> bool:
|
||||||
"""Return True if the category is directly "starred" by the current user."""
|
"""Return True if the category is directly "starred" by the current user."""
|
||||||
return category in self.context.get('starred_categories', [])
|
return category in self.context.get('starred_categories', [])
|
||||||
|
|
||||||
@ -723,7 +723,7 @@ class PartSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_starred(self, part):
|
def get_starred(self, part) -> bool:
|
||||||
"""Return "true" if the part is starred by the current user."""
|
"""Return "true" if the part is starred by the current user."""
|
||||||
return part in self.starred_parts
|
return part in self.starred_parts
|
||||||
|
|
||||||
|
@ -2,17 +2,25 @@
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions, serializers
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
|
|
||||||
class ActionPluginView(APIView):
|
class ActionPluginSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the ActionPluginView responses."""
|
||||||
|
|
||||||
|
action = serializers.CharField()
|
||||||
|
data = serializers.DictField()
|
||||||
|
|
||||||
|
|
||||||
|
class ActionPluginView(GenericAPIView):
|
||||||
"""Endpoint for running custom action plugins."""
|
"""Endpoint for running custom action plugins."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = ActionPluginSerializer
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""This function checks if all required info was submitted and then performs a plugin_action or returns an error."""
|
"""This function checks if all required info was submitted and then performs a plugin_action or returns an error."""
|
||||||
|
@ -1,22 +1,37 @@
|
|||||||
"""API for location plugins."""
|
"""API for location plugins."""
|
||||||
|
|
||||||
from rest_framework import permissions
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
|
from rest_framework import permissions, serializers
|
||||||
from rest_framework.exceptions import NotFound, ParseError
|
from rest_framework.exceptions import NotFound, ParseError
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
|
|
||||||
class LocatePluginView(APIView):
|
class LocatePluginSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the LocatePluginView API endpoint."""
|
||||||
|
|
||||||
|
plugin = serializers.CharField(
|
||||||
|
help_text='Plugin to use for location identification'
|
||||||
|
)
|
||||||
|
item = serializers.IntegerField(required=False, help_text='StockItem to identify')
|
||||||
|
location = serializers.IntegerField(
|
||||||
|
required=False, help_text='StockLocation to identify'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LocatePluginView(GenericAPIView):
|
||||||
"""Endpoint for using a custom plugin to identify or 'locate' a stock item or location."""
|
"""Endpoint for using a custom plugin to identify or 'locate' a stock item or location."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = LocatePluginSerializer
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Check inputs and offload the task to the plugin."""
|
"""Identify or 'locate' a stock item or location with a plugin."""
|
||||||
|
# Check inputs and offload the task to the plugin
|
||||||
# Which plugin to we wish to use?
|
# Which plugin to we wish to use?
|
||||||
plugin = request.data.get('plugin', None)
|
plugin = request.data.get('plugin', None)
|
||||||
|
|
||||||
|
@ -247,6 +247,7 @@ class NotificationUserSettingSerializer(GenericReferencedSettingSerializer):
|
|||||||
EXTRA_FIELDS = ['method']
|
EXTRA_FIELDS = ['method']
|
||||||
|
|
||||||
method = serializers.CharField(read_only=True)
|
method = serializers.CharField(read_only=True)
|
||||||
|
typ = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class PluginRegistryErrorSerializer(serializers.Serializer):
|
class PluginRegistryErrorSerializer(serializers.Serializer):
|
||||||
|
@ -184,7 +184,7 @@ class StockLocation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self) -> str:
|
||||||
"""Get the current icon used for this location.
|
"""Get the current icon used for this location.
|
||||||
|
|
||||||
The icon field on this model takes precedences over the possibly assigned stock location type
|
The icon field on this model takes precedences over the possibly assigned stock location type
|
||||||
|
@ -9,6 +9,7 @@ from django.urls import include, path, re_path
|
|||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from dj_rest_auth.views import LogoutView
|
from dj_rest_auth.views import LogoutView
|
||||||
|
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
|
||||||
from rest_framework import exceptions, permissions
|
from rest_framework import exceptions, permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@ -110,6 +111,7 @@ class RoleDetails(APIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Return the list of roles / permissions available to the current user."""
|
"""Return the list of roles / permissions available to the current user."""
|
||||||
@ -203,10 +205,17 @@ class GroupList(ListCreateAPI):
|
|||||||
ordering_fields = ['name']
|
ordering_fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
post=extend_schema(
|
||||||
|
responses={200: OpenApiResponse(description='User successfully logged out')}
|
||||||
|
)
|
||||||
|
)
|
||||||
class Logout(LogoutView):
|
class Logout(LogoutView):
|
||||||
"""API view for logging out via API."""
|
"""API view for logging out via API."""
|
||||||
|
|
||||||
def logout(self, request):
|
serializer_class = None
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
"""Logout the current user.
|
"""Logout the current user.
|
||||||
|
|
||||||
Deletes user token associated with request.
|
Deletes user token associated with request.
|
||||||
@ -230,6 +239,7 @@ class GetAuthToken(APIView):
|
|||||||
"""Return authentication token for an authenticated user."""
|
"""Return authentication token for an authenticated user."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Return an API token if the user is authenticated.
|
"""Return an API token if the user is authenticated.
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"""DRF API serializers for the 'users' app."""
|
"""DRF API serializers for the 'users' app."""
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group, User
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
|
|
||||||
from .models import Owner
|
from .models import Owner, RuleSet, check_user_role
|
||||||
|
|
||||||
|
|
||||||
class OwnerSerializer(InvenTreeModelSerializer):
|
class OwnerSerializer(InvenTreeModelSerializer):
|
||||||
@ -31,3 +31,39 @@ class GroupSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ['pk', 'name']
|
fields = ['pk', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class RoleSerializer(InvenTreeModelSerializer):
|
||||||
|
"""Serializer for a roles associated with a given user."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
model = User
|
||||||
|
fields = ['user', 'username', 'is_staff', 'is_superuser', 'roles']
|
||||||
|
|
||||||
|
user = serializers.IntegerField(source='pk')
|
||||||
|
username = serializers.CharField()
|
||||||
|
is_staff = serializers.BooleanField()
|
||||||
|
is_superuser = serializers.BooleanField()
|
||||||
|
roles = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_roles(self, user: User) -> dict:
|
||||||
|
"""Return roles associated with the specified User."""
|
||||||
|
roles = {}
|
||||||
|
|
||||||
|
for ruleset in RuleSet.RULESET_CHOICES:
|
||||||
|
role, _text = ruleset
|
||||||
|
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
for permission in RuleSet.RULESET_PERMISSIONS:
|
||||||
|
if check_user_role(user, role, permission):
|
||||||
|
permissions.append(permission)
|
||||||
|
|
||||||
|
if len(permissions) > 0:
|
||||||
|
roles[role] = permissions
|
||||||
|
else:
|
||||||
|
roles[role] = None # pragma: no cover
|
||||||
|
|
||||||
|
return roles
|
||||||
|
10
tasks.py
10
tasks.py
@ -894,10 +894,16 @@ def setup_test(c, ignore_update=False, dev=False, path='inventree-demo-dataset')
|
|||||||
'overwrite': 'Overwrite existing files without asking first (default = off/False)',
|
'overwrite': 'Overwrite existing files without asking first (default = off/False)',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def schema(c, filename='schema.yml', overwrite=False):
|
def schema(c, filename='schema.yml', overwrite=False, ignore_warnings=False):
|
||||||
"""Export current API schema."""
|
"""Export current API schema."""
|
||||||
check_file_existance(filename, overwrite)
|
check_file_existance(filename, overwrite)
|
||||||
manage(c, f'spectacular --file {filename}')
|
|
||||||
|
cmd = f'spectacular --file {filename} --validate --color'
|
||||||
|
|
||||||
|
if not ignore_warnings:
|
||||||
|
cmd += ' --fail-on-warn'
|
||||||
|
|
||||||
|
manage(c, cmd, pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task(default=True)
|
@task(default=True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user