mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-27 19:16:44 +00:00
fix(backend): repair remaining schema generation errors (#9453)
* Remove hardcoded currency enum from schema * Convert schema custom key enums to int to allow customized keys to validate * Convert stock status key enums to int to allow customizations to validate in schema * api version bump * fix remaining operationId errors * fix errors * fix another error * fix missing model * ensure we do not ignore warnings anymore * Restore enumerated help text for currencies * Remove commented block of old code * Restore custom key enumerated values to schema documentation * Restore status key enumeration to schema documentation * fix more enums * Add debug definitions for schema generation * fix schema generation for PluginRelationSerializer * add migrations * fix enum names for allauth schema duplications * bump api version --------- Co-authored-by: Joe Rogers <1337joe@gmail.com>
This commit is contained in:
parent
f87f4387ed
commit
0d6c47fcd5
1
.github/workflows/qc_checks.yaml
vendored
1
.github/workflows/qc_checks.yaml
vendored
@ -193,7 +193,6 @@ jobs:
|
||||
diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
|
||||
- name: Check schema - including warnings
|
||||
run: invoke dev.schema
|
||||
continue-on-error: true
|
||||
- name: Extract version for publishing
|
||||
id: version
|
||||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
|
||||
|
22
.vscode/launch.json
vendored
22
.vscode/launch.json
vendored
@ -40,6 +40,28 @@
|
||||
"django": true,
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "InvenTree invoke schema",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/.venv/lib/python3.9/site-packages/invoke/__main__.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"dev.schema","--ignore-warnings"
|
||||
],
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "schema generation",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
|
||||
"args": [
|
||||
"schema",
|
||||
"--file","src/frontend/schema.yml"
|
||||
],
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "InvenTree Frontend - Vite",
|
||||
"type": "chrome",
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 333
|
||||
INVENTREE_API_VERSION = 334
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v334 - 2025-04-08 : https://github.com/inventree/InvenTree/pull/9453
|
||||
- Fixes various operationId and enum collisions and help texts
|
||||
|
||||
v333 - 2025-04-03 : https://github.com/inventree/InvenTree/pull/9452
|
||||
- Currency string is no longer restricted to a hardcoded enum
|
||||
- Customizable status keys are no longer hardcoded enum values
|
||||
|
@ -10,16 +10,19 @@ from drf_spectacular.utils import _SchemaType
|
||||
class ExtendedAutoSchema(AutoSchema):
|
||||
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
|
||||
|
||||
def is_bulk_delete(self) -> bool:
|
||||
"""Check the class of the current view for the BulkDeleteMixin."""
|
||||
return 'BulkDeleteMixin' in [c.__name__ for c in type(self.view).__mro__]
|
||||
def is_bulk_action(self, ref: str) -> bool:
|
||||
"""Check the class of the current view for the bulk mixins."""
|
||||
return ref in [c.__name__ for c in type(self.view).__mro__]
|
||||
|
||||
def get_operation_id(self) -> str:
|
||||
"""Custom path handling overrides, falling back to default behavior."""
|
||||
result_id = super().get_operation_id()
|
||||
|
||||
# rename bulk deletes to deconflict with single delete operation_id
|
||||
if self.method == 'DELETE' and self.is_bulk_delete():
|
||||
# rename bulk actions to deconflict with single action operation_id
|
||||
if (self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin')) or (
|
||||
(self.method == 'PUT' or self.method == 'PATCH')
|
||||
and self.is_bulk_action('BulkUpdateMixin')
|
||||
):
|
||||
action = self.method_mapping[self.method.lower()]
|
||||
result_id = result_id.replace(action, 'bulk_' + action)
|
||||
|
||||
@ -42,7 +45,7 @@ class ExtendedAutoSchema(AutoSchema):
|
||||
|
||||
# drf-spectacular doesn't support a body on DELETE endpoints because the semantics are not well-defined and
|
||||
# OpenAPI recommends against it. This allows us to generate a schema that follows existing behavior.
|
||||
if self.method == 'DELETE' and self.is_bulk_delete():
|
||||
if self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin'):
|
||||
original_method = self.method
|
||||
self.method = 'PUT'
|
||||
request_body = self._get_request_body()
|
||||
|
@ -1435,6 +1435,15 @@ SPECTACULAR_SETTINGS = {
|
||||
'drf_spectacular.hooks.postprocess_schema_enums',
|
||||
'InvenTree.schema.postprocess_required_nullable',
|
||||
],
|
||||
'ENUM_NAME_OVERRIDES': {
|
||||
'UserTypeEnum': 'users.models.UserProfile.UserType',
|
||||
'TemplateModelTypeEnum': 'report.models.ReportTemplateBase.ModelChoices',
|
||||
'AttachmentModelTypeEnum': 'common.models.Attachment.ModelChoices',
|
||||
'DataImportSessionModelTypeEnum': 'importer.models.DataImportSession.ModelChoices',
|
||||
# Allauth
|
||||
'UnauthorizedStatus': [[401, 401]],
|
||||
'IsTrueEnum': [[True, True]],
|
||||
},
|
||||
}
|
||||
|
||||
if SITE_URL and not TESTING:
|
||||
|
@ -699,7 +699,6 @@ class ContentTypeDetail(RetrieveAPI):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
@extend_schema(operation_id='contenttype_retrieve_model')
|
||||
class ContentTypeModelDetail(ContentTypeDetail):
|
||||
"""Detail view for a ContentType model."""
|
||||
|
||||
@ -714,6 +713,11 @@ class ContentTypeModelDetail(ContentTypeDetail):
|
||||
raise NotFound()
|
||||
raise NotFound()
|
||||
|
||||
@extend_schema(operation_id='contenttype_retrieve_model')
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Detail view for a ContentType model."""
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AttachmentFilter(rest_filters.FilterSet):
|
||||
"""Filterset for the AttachmentList API endpoint."""
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.20 on 2025-04-07 20:53
|
||||
|
||||
import common.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("common", "0037_dataoutput"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="attachment",
|
||||
name="model_type",
|
||||
field=models.CharField(
|
||||
help_text="Target model type for image",
|
||||
max_length=100,
|
||||
validators=[common.validators.validate_attachment_model_type],
|
||||
verbose_name="Model type",
|
||||
),
|
||||
),
|
||||
]
|
@ -40,15 +40,11 @@ from djmoney.contrib.exchange.models import convert_money
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.currency
|
||||
import common.validators
|
||||
import InvenTree.exceptions
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import InvenTree.validators
|
||||
import users.models
|
||||
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
|
||||
from generic.states import ColorEnum
|
||||
@ -59,6 +55,24 @@ from InvenTree.sanitizer import sanitize_svg
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class RenderMeta(models.enums.ChoicesMeta):
|
||||
"""Metaclass for rendering choices."""
|
||||
|
||||
choice_fnc = None
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
"""Return a list of choices for the enum class."""
|
||||
fnc = getattr(self, 'choice_fnc', None)
|
||||
if fnc:
|
||||
return fnc()
|
||||
return []
|
||||
|
||||
|
||||
class RenderChoices(models.TextChoices, metaclass=RenderMeta):
|
||||
"""Class for creating enumerated string choices for schema rendering."""
|
||||
|
||||
|
||||
class MetaMixin(models.Model):
|
||||
"""A base class for InvenTree models to include shared meta fields.
|
||||
|
||||
@ -1811,6 +1825,11 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
|
||||
|
||||
verbose_name = _('Attachment')
|
||||
|
||||
class ModelChoices(RenderChoices):
|
||||
"""Model choices for attachments."""
|
||||
|
||||
choice_fnc = common.validators.attachment_model_options
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom 'save' method for the Attachment model.
|
||||
|
||||
@ -1859,7 +1878,8 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
|
||||
model_type = models.CharField(
|
||||
max_length=100,
|
||||
validators=[common.validators.validate_attachment_model_type],
|
||||
help_text=_('Target model type for this image'),
|
||||
verbose_name=_('Model type'),
|
||||
help_text=_('Target model type for image'),
|
||||
)
|
||||
|
||||
model_id = models.PositiveIntegerField()
|
||||
|
@ -99,6 +99,7 @@ class AllStatusViews(StatusView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
@extend_schema(operation_id='generic_status_retrieve_all')
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
from InvenTree.helpers import inheritors
|
||||
|
@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.urls import include, path
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import permissions
|
||||
from rest_framework import permissions, serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@ -56,10 +56,19 @@ class DataImporterPermissionMixin:
|
||||
permission_classes = [permissions.IsAuthenticated, DataImporterPermission]
|
||||
|
||||
|
||||
class DataImporterModelSerializer(serializers.Serializer):
|
||||
"""Model references to map info that might get imported."""
|
||||
|
||||
serializer = serializers.CharField(read_only=True)
|
||||
model_type = serializers.CharField(read_only=True)
|
||||
api_url = serializers.URLField(read_only=True)
|
||||
|
||||
|
||||
class DataImporterModelList(APIView):
|
||||
"""API endpoint for displaying a list of models available for import."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = DataImporterModelSerializer(many=True)
|
||||
|
||||
def get(self, request):
|
||||
"""Return a list of models available for import."""
|
||||
@ -102,6 +111,7 @@ class DataImportSessionAcceptFields(APIView):
|
||||
"""API endpoint to accept the field mapping for a DataImportSession."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = None
|
||||
|
||||
@extend_schema(
|
||||
responses={200: importer.serializers.DataImportSessionSerializer(many=False)}
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.20 on 2025-04-07 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
import importer.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("importer", "0003_dataimportsession_field_filters"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="dataimportsession",
|
||||
name="model_type",
|
||||
field=models.CharField(
|
||||
help_text="Target model type for this import session",
|
||||
max_length=100,
|
||||
validators=[importer.validators.validate_importer_model_type],
|
||||
verbose_name="Model Type",
|
||||
),
|
||||
),
|
||||
]
|
@ -18,6 +18,7 @@ import importer.registry
|
||||
import importer.tasks
|
||||
import importer.validators
|
||||
import InvenTree.helpers
|
||||
from common.models import RenderChoices
|
||||
from importer.status_codes import DataImportStatusCode
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
@ -38,6 +39,11 @@ class DataImportSession(models.Model):
|
||||
field_filters: JSONField for field filter values - optional field API filters
|
||||
"""
|
||||
|
||||
class ModelChoices(RenderChoices):
|
||||
"""Model choices for data import sessions."""
|
||||
|
||||
choice_fnc = importer.registry.supported_models
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the DataImportSession model."""
|
||||
@ -77,6 +83,8 @@ class DataImportSession(models.Model):
|
||||
blank=False,
|
||||
max_length=100,
|
||||
validators=[importer.validators.validate_importer_model_type],
|
||||
verbose_name=_('Model Type'),
|
||||
help_text=_('Target model type for this import session'),
|
||||
)
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
|
@ -292,6 +292,15 @@ class PluginSettingList(ListAPI):
|
||||
|
||||
filterset_fields = ['plugin__active', 'plugin__key']
|
||||
|
||||
@extend_schema(operation_id='plugins_settings_list_all')
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""List endpoint for all plugin related settings.
|
||||
|
||||
- read only
|
||||
- only accessible by staff users
|
||||
"""
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
def check_plugin(
|
||||
plugin_slug: Optional[str], plugin_pk: Optional[int]
|
||||
|
@ -3,6 +3,8 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import GenericReferencedSettingSerializer
|
||||
@ -307,6 +309,7 @@ class PluginRegistryStatusSerializer(serializers.Serializer):
|
||||
registry_errors = serializers.ListField(child=PluginRegistryErrorSerializer())
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class PluginRelationSerializer(serializers.PrimaryKeyRelatedField):
|
||||
"""Serializer for a plugin field. Uses the 'slug' of the plugin as the lookup."""
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.20 on 2025-04-07 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
import report.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("report", "0029_remove_reportoutput_template_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="labeltemplate",
|
||||
name="model_type",
|
||||
field=models.CharField(
|
||||
help_text="Target model type for template",
|
||||
max_length=100,
|
||||
validators=[report.validators.validate_report_model_type],
|
||||
verbose_name="Model Type",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="reporttemplate",
|
||||
name="model_type",
|
||||
field=models.CharField(
|
||||
help_text="Target model type for template",
|
||||
max_length=100,
|
||||
validators=[report.validators.validate_report_model_type],
|
||||
verbose_name="Model Type",
|
||||
),
|
||||
),
|
||||
]
|
@ -27,7 +27,7 @@ import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import report.helpers
|
||||
import report.validators
|
||||
from common.models import DataOutput
|
||||
from common.models import DataOutput, RenderChoices
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.helpers_model import get_base_url
|
||||
from InvenTree.models import MetadataMixin
|
||||
@ -179,6 +179,11 @@ class ReportContextExtension(TypedDict):
|
||||
class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
"""Base class for reports, labels."""
|
||||
|
||||
class ModelChoices(RenderChoices):
|
||||
"""Model choices for report templates."""
|
||||
|
||||
choice_fnc = report.helpers.report_model_options
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
@ -269,6 +274,7 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
model_type = models.CharField(
|
||||
max_length=100,
|
||||
validators=[report.validators.validate_report_model_type],
|
||||
verbose_name=_('Model Type'),
|
||||
help_text=_('Target model type for template'),
|
||||
)
|
||||
|
||||
|
@ -33,7 +33,6 @@ import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import report.mixins
|
||||
import report.models
|
||||
import stock.tasks
|
||||
from common.icons import validate_icon
|
||||
from common.settings import get_global_setting
|
||||
|
@ -328,8 +328,8 @@ class TokenListView(TokenMixin, ListCreateAPI):
|
||||
'revoked',
|
||||
'revoked',
|
||||
]
|
||||
|
||||
filterset_fields = ['revoked', 'user']
|
||||
queryset = ApiToken.objects.none()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create token and show key to user."""
|
||||
|
@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.20 on 2025-04-07 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0014_userprofile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("bot", "Bot"),
|
||||
("internal", "Internal"),
|
||||
("external", "External"),
|
||||
("guest", "Guest"),
|
||||
],
|
||||
default="internal",
|
||||
help_text="Which type of user is this?",
|
||||
max_length=10,
|
||||
verbose_name="User Type",
|
||||
),
|
||||
),
|
||||
]
|
@ -1025,7 +1025,7 @@ class UserProfile(InvenTree.models.MetadataMixin):
|
||||
max_length=10,
|
||||
choices=UserType.choices,
|
||||
default=UserType.INTERNAL,
|
||||
verbose_name=_('Type'),
|
||||
verbose_name=_('User Type'),
|
||||
help_text=_('Which type of user is this?'),
|
||||
)
|
||||
organisation = models.CharField(
|
||||
|
Loading…
x
Reference in New Issue
Block a user