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:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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')
|
||||
|
Reference in New Issue
Block a user