2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-03 15:52:51 +00:00

Refactor query parameters handling for SupplierPart and StockList endpoints (#10374)

* refactor output options for SupplierPart endpoints

* Add output options schema generation and corresponding tests for SupplierPart

* Bump API version to 392 and update version history; remove unused parameters in SupplierPartMixin

* Update API version to 392 and adjust version history with new release date

* Enhance output options handling in SupplierPart API

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Reza
2025-09-24 02:45:28 +03:30
committed by GitHub
parent a7b1b9d523
commit 4681cbbfef
6 changed files with 151 additions and 24 deletions

View File

@@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 391
INVENTREE_API_VERSION = 392
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v392 -> 2025-09-22 : https://github.com/inventree/InvenTree/pull/10374
- Refactors 'part_detail', 'supplier_detail', 'manufacturer_detail'and 'pretty' param in SupplierPartList API endpoint
v391 -> 2025-09-06 : https://github.com/inventree/InvenTree/pull/10279
- Refactors 'exclude_tree', 'cascade', and 'location' filters in StockList API endpoint

View File

@@ -212,3 +212,32 @@ class InvenTreeNotesField(models.TextField):
kwargs['null'] = True
super().__init__(**kwargs)
class InvenTreeOutputOption:
"""Represents an available output option with description, flag name, and default value."""
def __init__(self, description: str, flag: str, default=None):
"""Initialize the output option."""
self.description = description
self.flag = flag
self.default = default
class OutputConfiguration:
"""Holds all available output options for a view.
This class is responsible for converting incoming query parameters from an API request
into a dictionary of boolean flags, which can then be applied to serializers.
"""
OPTIONS: list[InvenTreeOutputOption]
@classmethod
def format_params(cls, params: dict) -> dict[str, bool]:
"""Convert query parameters into a dictionary of output flags with boolean values."""
result = {}
for option in cls.OPTIONS:
value = params.get(option.flag, option.default)
result[option.flag] = InvenTree.helpers.str2bool(value)
return result

View File

@@ -7,12 +7,13 @@ from rest_framework.response import Response
import data_exporter.mixins
import importer.mixins
from InvenTree.fields import InvenTreeNotesField
from InvenTree.fields import InvenTreeNotesField, OutputConfiguration
from InvenTree.helpers import (
clean_markdown,
remove_non_printable_characters,
strip_html_tags,
)
from InvenTree.schema import schema_for_view_output_options
class CleanMixin:
@@ -206,3 +207,27 @@ class DataImportExportSerializerMixin(
importer.mixins.DataImportSerializerMixin,
):
"""Mixin class for adding data import/export functionality to a DRF serializer."""
class OutputOptionsMixin:
"""Mixin to handle output options for API endpoints."""
output_options: OutputConfiguration
def __init_subclass__(cls, **kwargs):
"""Automatically attaches OpenAPI schema parameters for its output options."""
super().__init_subclass__(**kwargs)
schema_for_view_output_options(cls)
def get_serializer(self, *args, **kwargs):
"""Return serializer instance with output options applied."""
if not hasattr(self, 'output_options'):
raise AttributeError(
f"Class {self.__class__.__name__} must define 'output_options' attribute"
)
if self.output_options and hasattr(self, 'request'):
params = self.request.query_params
kwargs.update(self.output_options.format_params(params))
return super().get_serializer(*args, **kwargs)

View File

@@ -9,7 +9,13 @@ from drf_spectacular.contrib.django_oauth_toolkit import DjangoOAuthToolkitSchem
from drf_spectacular.drainage import warn
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import ComponentRegistry
from drf_spectacular.utils import _SchemaType
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
_SchemaType,
extend_schema,
extend_schema_view,
)
from rest_framework.pagination import LimitOffsetPagination
from InvenTree.permissions import OASTokenMixin
@@ -211,3 +217,29 @@ def postprocess_print_stats(result, generator, request, public):
)
return result
def schema_for_view_output_options(view_class):
"""A class decorator that automatically generates schema parameters for a view.
It works by introspecting the `output_options` attribute on the view itself.
This decorator reads the `output_options` attribute from the view class,
extracts the `OPTIONS` list from it, and creates an OpenApiParameter for each option.
"""
output_config_class = view_class.output_options
parameters = []
for option in output_config_class.OPTIONS:
param = OpenApiParameter(
name=option.flag,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.QUERY,
description=option.description,
default=option.default,
)
parameters.append(param)
extended_view = extend_schema_view(get=extend_schema(parameters=parameters))(
view_class
)
return extended_view

View File

@@ -10,9 +10,10 @@ from django_filters.rest_framework.filterset import FilterSet
import part.models
from data_exporter.mixins import DataExportViewMixin
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.helpers import str2bool
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin, RetrieveUpdateDestroyAPI
from .models import (
Address,
@@ -325,6 +326,33 @@ class SupplierPartFilter(FilterSet):
return queryset.exclude(in_stock__gt=0)
class SupplierPartOutputOptions(OutputConfiguration):
"""Available output options for the SupplierPart endpoints."""
OPTIONS = [
InvenTreeOutputOption(
description='Include detailed information about the linked Part in the response',
flag='part_detail',
default=False,
),
InvenTreeOutputOption(
description='Include detailed information about the Supplier in the response',
flag='supplier_detail',
default=True,
),
InvenTreeOutputOption(
description='Include detailed information about the Manufacturer in the response',
flag='manufacturer_detail',
default=False,
),
InvenTreeOutputOption(
description='Format the output with a more readable (pretty) name',
flag='pretty',
default=False,
),
]
class SupplierPartMixin:
"""Mixin class for SupplierPart API endpoints."""
@@ -342,27 +370,9 @@ class SupplierPartMixin:
return queryset
def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint."""
# Do we wish to include extra detail?
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', True))
kwargs['manufacturer_detail'] = str2bool(
params.get('manufacturer_detail', None)
)
kwargs['pretty'] = str2bool(params.get('pretty', None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return super().get_serializer(*args, **kwargs)
class SupplierPartList(
DataExportViewMixin, SupplierPartMixin, ListCreateDestroyAPIView
DataExportViewMixin, SupplierPartMixin, OutputOptionsMixin, ListCreateDestroyAPIView
):
"""API endpoint for list view of SupplierPart object.
@@ -373,6 +383,7 @@ class SupplierPartList(
filterset_class = SupplierPartFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SupplierPartOutputOptions
ordering_fields = [
'SKU',
@@ -410,7 +421,9 @@ class SupplierPartList(
]
class SupplierPartDetail(SupplierPartMixin, RetrieveUpdateDestroyAPI):
class SupplierPartDetail(
SupplierPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of SupplierPart object.
- GET: Retrieve detail view
@@ -418,6 +431,8 @@ class SupplierPartDetail(SupplierPartMixin, RetrieveUpdateDestroyAPI):
- DELETE: Delete object
"""
output_options = SupplierPartOutputOptions
class SupplierPriceBreakFilter(FilterSet):
"""Custom API filters for the SupplierPriceBreak list endpoint."""

View File

@@ -565,6 +565,29 @@ class SupplierPartTest(InvenTreeAPITestCase):
response = self.get(url, {'part': pk}, expected_code=200)
self.assertEqual(len(response.data), n)
def test_output_options(self):
"""Test the output options for SupplierPart detail."""
sp = SupplierPart.objects.all().first()
url = reverse('api-supplier-part-detail', kwargs={'pk': sp.pk})
response = self.get(url, {'part_detail': 'true'}, expected_code=200)
self.assertIn('part_detail', response.data)
response = self.get(url, {'supplier_detail': 'true'}, expected_code=200)
self.assertIn('supplier_detail', response.data)
response = self.get(url, {'supplier_detail': 'false'}, expected_code=200)
self.assertNotIn('supplier_detail', response.data)
response = self.get(url, {'manufacturer_detail': 'true'}, expected_code=200)
self.assertIn('manufacturer_detail', response.data)
response = self.get(url, {'pretty': 'true'}, expected_code=200)
self.assertIn('pretty_name', response.data)
response = self.get(url, {'pretty': 'false'}, expected_code=200)
self.assertNotIn('pretty_name', response.data)
def test_available(self):
"""Tests for updating the 'available' field."""
url = reverse('api-supplier-part-list')