mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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
|
||||
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:
|
||||
name: Tests - inventree-python
|
||||
runs-on: ubuntu-20.04
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -48,6 +48,7 @@ label.pdf
|
||||
label.png
|
||||
InvenTree/my_special*
|
||||
_tests*.txt
|
||||
schema.yml
|
||||
|
||||
# Local static and media file storage (only when running in development mode)
|
||||
inventree_media
|
||||
@ -70,6 +71,7 @@ secret_key.txt
|
||||
.idea/
|
||||
*.code-workspace
|
||||
.bash_history
|
||||
.DS_Store
|
||||
|
||||
# https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
.vscode/*
|
||||
@ -107,5 +109,8 @@ InvenTree/plugins/
|
||||
*.mo
|
||||
messages.ts
|
||||
|
||||
# Generated API schema file
|
||||
api.yaml
|
||||
|
||||
# web frontend (static files)
|
||||
InvenTree/web/static
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Main JSON interface views."""
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
@ -8,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_q.models import OrmQ
|
||||
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.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
@ -18,6 +21,7 @@ from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import ListCreateAPI
|
||||
from InvenTree.permissions import RolePermission
|
||||
from InvenTree.templatetags.inventree_extras import plugins_info
|
||||
from part.models import Part
|
||||
from plugin.serializers import MetadataSerializer
|
||||
from users.models import ApiToken
|
||||
|
||||
@ -28,11 +32,41 @@ from .version import inventreeApiText
|
||||
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):
|
||||
"""Simple JSON endpoint for InvenTree version information."""
|
||||
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
|
||||
@extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)})
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return information about the InvenTree server."""
|
||||
return JsonResponse({
|
||||
@ -81,6 +115,8 @@ class VersionApiSerializer(serializers.Serializer):
|
||||
class VersionTextView(ListAPI):
|
||||
"""Simple JSON endpoint for InvenTree version text."""
|
||||
|
||||
serializer_class = VersionSerializer
|
||||
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
|
||||
@extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)})
|
||||
@ -324,7 +360,17 @@ class AttachmentMixin:
|
||||
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.
|
||||
|
||||
Returns hits against a number of different models simultaneously,
|
||||
@ -334,6 +380,7 @@ class APISearchView(APIView):
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = APISearchViewSerializer
|
||||
|
||||
def get_result_types(self):
|
||||
"""Construct a list of search types we can return."""
|
||||
@ -446,4 +493,7 @@ class MetadataView(RetrieveUpdateAPI):
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""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)
|
||||
|
@ -9,8 +9,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import sesame.utils
|
||||
from rest_framework import serializers
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
import InvenTree.version
|
||||
|
||||
@ -38,7 +38,7 @@ class GetSimpleLoginSerializer(serializers.Serializer):
|
||||
email = serializers.CharField(label=_('Email'))
|
||||
|
||||
|
||||
class GetSimpleLoginView(APIView):
|
||||
class GetSimpleLoginView(GenericAPIView):
|
||||
"""View to send a simple login link."""
|
||||
|
||||
permission_classes = ()
|
||||
|
@ -28,6 +28,10 @@ from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||
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):
|
||||
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
|
||||
|
||||
|
@ -517,12 +517,15 @@ if USE_JWT:
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'InvenTree API',
|
||||
'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system',
|
||||
'LICENSE': {'MIT': 'https://github.com/inventree/InvenTree/blob/master/LICENSE'},
|
||||
'EXTERNAL_DOCS': {
|
||||
'docs': 'https://docs.inventree.org',
|
||||
'web': 'https://inventree.org',
|
||||
'LICENSE': {
|
||||
'name': 'MIT',
|
||||
'url': 'https://github.com/inventree/InvenTree/blob/master/LICENSE',
|
||||
},
|
||||
'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,
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ from allauth.account.models import EmailAddress
|
||||
from allauth.socialaccount import providers
|
||||
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
@ -16,7 +17,7 @@ from rest_framework.response import Response
|
||||
import InvenTree.sso
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@ -112,11 +113,36 @@ for name, provider in providers.registry.provider_map.items():
|
||||
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):
|
||||
"""List of available social providers."""
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
@extend_schema(
|
||||
responses={200: OpenApiResponse(response=SocialProviderListResponseSerializer)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Get the list of providers."""
|
||||
provider_list = []
|
||||
|
@ -18,6 +18,11 @@ class ApiVersionTests(InvenTreeAPITestCase):
|
||||
|
||||
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):
|
||||
"""Test that the inventreeApiText function works expected."""
|
||||
# Normal run
|
||||
|
@ -11,6 +11,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
import django_q.models
|
||||
from django_q.tasks import async_task
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from error_report.models import Error
|
||||
from rest_framework import permissions, serializers
|
||||
from rest_framework.exceptions import NotAcceptable, NotFound
|
||||
@ -53,7 +54,15 @@ class WebhookView(CsrfExemptMixin, APIView):
|
||||
permission_classes = []
|
||||
model_class = common.models.WebhookEndpoint
|
||||
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):
|
||||
"""Process incoming webhook."""
|
||||
# get webhook definition
|
||||
@ -115,6 +124,7 @@ class CurrencyExchangeView(APIView):
|
||||
"""API endpoint for displaying currency information."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""Return information on available currency conversions."""
|
||||
@ -157,6 +167,7 @@ class CurrencyRefreshView(APIView):
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""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."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""Return information about the current status of the background task queue."""
|
||||
|
@ -2868,7 +2868,7 @@ class NotificationMessage(models.Model):
|
||||
"""Return API endpoint."""
|
||||
return reverse('api-notifications-list')
|
||||
|
||||
def age(self):
|
||||
def age(self) -> int:
|
||||
"""Age of the message in seconds."""
|
||||
# Add timezone information if TZ is enabled (in production mode mostly)
|
||||
delta = now() - (
|
||||
@ -2878,7 +2878,7 @@ class NotificationMessage(models.Model):
|
||||
)
|
||||
return delta.seconds
|
||||
|
||||
def age_human(self):
|
||||
def age_human(self) -> str:
|
||||
"""Humanized age."""
|
||||
return naturaltime(self.creation)
|
||||
|
||||
|
@ -59,6 +59,8 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
|
||||
units = serializers.CharField(read_only=True)
|
||||
|
||||
typ = serializers.CharField(read_only=True)
|
||||
|
||||
def get_choices(self, obj):
|
||||
"""Returns the choices available for a given item."""
|
||||
results = []
|
||||
@ -195,7 +197,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
read = serializers.BooleanField()
|
||||
|
||||
def get_target(self, obj):
|
||||
def get_target(self, obj) -> dict:
|
||||
"""Function to resolve generic object reference to target."""
|
||||
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
|
||||
|
||||
@ -217,7 +219,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||
|
||||
return target
|
||||
|
||||
def get_source(self, obj):
|
||||
def get_source(self, obj) -> dict:
|
||||
"""Function to resolve generic object reference to source."""
|
||||
return get_objectreference(obj, 'source_content_type', 'source_object_id')
|
||||
|
||||
|
@ -2,15 +2,24 @@
|
||||
|
||||
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.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.serializers import EmptySerializer
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
if status_model is None:
|
||||
raise ValidationError(
|
||||
raise serializers.ValidationError(
|
||||
f"StatusView view called without '{self.MODEL_REF}' parameter"
|
||||
)
|
||||
|
||||
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):
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
status_class = self.get_status_model()
|
||||
@ -53,6 +69,7 @@ class AllStatusViews(StatusView):
|
||||
"""Endpoint for listing all defined status models."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
|
@ -49,6 +49,7 @@ from InvenTree.mixins import (
|
||||
UpdateAPI,
|
||||
)
|
||||
from InvenTree.permissions import RolePermission
|
||||
from InvenTree.serializers import EmptySerializer
|
||||
from InvenTree.status_codes import (
|
||||
BuildStatusGroups,
|
||||
PurchaseOrderStatusGroups,
|
||||
@ -487,6 +488,7 @@ class PartScheduling(RetrieveAPI):
|
||||
"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Return scheduling information for the referenced Part instance."""
|
||||
@ -687,6 +689,7 @@ class PartRequirements(RetrieveAPI):
|
||||
"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""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."""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""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
|
||||
# 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 = [
|
||||
star.part for star in self.request.user.starred_parts.all()
|
||||
]
|
||||
|
@ -748,7 +748,7 @@ class Part(
|
||||
return stock[0].serial
|
||||
|
||||
@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."""
|
||||
return part_helpers.render_part_full_name(self)
|
||||
|
||||
@ -762,7 +762,7 @@ class Part(
|
||||
return helpers.getMediaUrl(self.image.url)
|
||||
return helpers.getBlankImage()
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
def get_thumbnail_url(self) -> str:
|
||||
"""Return the URL of the image thumbnail for this part."""
|
||||
if self.image:
|
||||
return helpers.getMediaUrl(self.image.thumbnail.url)
|
||||
|
@ -91,7 +91,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
if not path_detail:
|
||||
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 category in self.context.get('starred_categories', [])
|
||||
|
||||
@ -723,7 +723,7 @@ class PartSerializer(
|
||||
|
||||
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 part in self.starred_parts
|
||||
|
||||
|
@ -2,17 +2,25 @@
|
||||
|
||||
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.views import APIView
|
||||
|
||||
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."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = ActionPluginSerializer
|
||||
|
||||
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."""
|
||||
|
@ -1,22 +1,37 @@
|
||||
"""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.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.tasks import offload_task
|
||||
from plugin.registry import registry
|
||||
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."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = LocatePluginSerializer
|
||||
|
||||
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?
|
||||
plugin = request.data.get('plugin', None)
|
||||
|
||||
|
@ -247,6 +247,7 @@ class NotificationUserSettingSerializer(GenericReferencedSettingSerializer):
|
||||
EXTRA_FIELDS = ['method']
|
||||
|
||||
method = serializers.CharField(read_only=True)
|
||||
typ = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class PluginRegistryErrorSerializer(serializers.Serializer):
|
||||
|
@ -184,7 +184,7 @@ class StockLocation(
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
"""Get the current icon used for this location.
|
||||
|
||||
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 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.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@ -110,6 +111,7 @@ class RoleDetails(APIView):
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return the list of roles / permissions available to the current user."""
|
||||
@ -203,10 +205,17 @@ class GroupList(ListCreateAPI):
|
||||
ordering_fields = ['name']
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
responses={200: OpenApiResponse(description='User successfully logged out')}
|
||||
)
|
||||
)
|
||||
class Logout(LogoutView):
|
||||
"""API view for logging out via API."""
|
||||
|
||||
def logout(self, request):
|
||||
serializer_class = None
|
||||
|
||||
def post(self, request):
|
||||
"""Logout the current user.
|
||||
|
||||
Deletes user token associated with request.
|
||||
@ -230,6 +239,7 @@ class GetAuthToken(APIView):
|
||||
"""Return authentication token for an authenticated user."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return an API token if the user is authenticated.
|
||||
|
@ -1,12 +1,12 @@
|
||||
"""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 InvenTree.serializers import InvenTreeModelSerializer
|
||||
|
||||
from .models import Owner
|
||||
from .models import Owner, RuleSet, check_user_role
|
||||
|
||||
|
||||
class OwnerSerializer(InvenTreeModelSerializer):
|
||||
@ -31,3 +31,39 @@ class GroupSerializer(InvenTreeModelSerializer):
|
||||
|
||||
model = Group
|
||||
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)',
|
||||
}
|
||||
)
|
||||
def schema(c, filename='schema.yml', overwrite=False):
|
||||
def schema(c, filename='schema.yml', overwrite=False, ignore_warnings=False):
|
||||
"""Export current API schema."""
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user