From 6bf2bbbb630218acf7a1d75fa7fae510332df07c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 13 Nov 2026 03:14:49 +0100 Subject: [PATCH] feat(backend): use a generic registration approach for search defintion (#13239) * use a generic registration approach for search defintion this is in theory open to plugins but not scope for this PR * reduce unneeded fn wrap * simplify * increase max_time by 1 sec --- src/backend/InvenTree/InvenTree/api.py | 31 ++---------------- src/backend/InvenTree/build/api.py | 2 ++ src/backend/InvenTree/build/test_api.py | 8 ++--- src/backend/InvenTree/company/api.py | 11 +++++++ .../InvenTree/generic/search/__init__.py | 5 +++ src/backend/InvenTree/generic/search/func.py | 32 +++++++++++++++++++ src/backend/InvenTree/order/api.py | 5 +++ .../InvenTree/order/test_sales_order.py | 2 +- src/backend/InvenTree/part/api.py | 3 ++ src/backend/InvenTree/part/test_api.py | 2 +- src/backend/InvenTree/stock/api.py | 3 ++ 11 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 src/backend/InvenTree/generic/search/__init__.py create mode 100644 src/backend/InvenTree/generic/search/func.py diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index 0a31f8dbf9..7f0be1585b 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -32,6 +32,7 @@ import InvenTree.permissions import InvenTree.version from audit.helpers import write_batched from common.settings import get_global_setting +from generic.search import get_search_filters, get_search_providers from InvenTree import helpers, ready from InvenTree.auth_overrides import registration_enabled from InvenTree.mixins import ListCreateAPI @@ -797,37 +798,11 @@ class APISearchView(GenericAPIView): def get_result_types(self): """Construct a list of search types we can return.""" - import build.api - import company.api - import order.api - import part.api - import stock.api - - return { - 'build': build.api.BuildList, - 'company': company.api.CompanyList, - 'supplier': company.api.CompanyList, - 'manufacturer': company.api.CompanyList, - 'customer': company.api.CompanyList, - 'manufacturerpart': company.api.ManufacturerPartList, - 'supplierpart': company.api.SupplierPartList, - 'part': part.api.PartList, - 'partcategory': part.api.CategoryList, - 'purchaseorder': order.api.PurchaseOrderViewSet, - 'returnorder': order.api.ReturnOrderList, - 'salesorder': order.api.SalesOrderList, - 'salesordershipment': order.api.SalesOrderShipmentList, - 'stockitem': stock.api.StockList, - 'stocklocation': stock.api.StockLocationList, - } + return get_search_providers() def get_result_filters(self): """Provide extra filtering options for particular search groups.""" - return { - 'supplier': {'is_supplier': True}, - 'manufacturer': {'is_manufacturer': True}, - 'customer': {'is_customer': True}, - } + return get_search_filters() def post(self, request, *args, **kwargs): """Perform search query against available models.""" diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 8b4ab9b3dd..4ca53fc915 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -34,6 +34,7 @@ import stock.serializers from build.models import Build, BuildItem, BuildLine from build.status_codes import BuildStatus, BuildStatusGroups from data_exporter.mixins import DataExportViewMixin +from generic.search import search_provider from generic.states.api import StatusView from InvenTree.api import BulkDeleteMixin, ParameterListMixin, meta_path from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration @@ -341,6 +342,7 @@ class BuildListOutputOptions(OutputConfiguration): OPTIONS = [InvenTreeOutputOption('part_detail', default=True)] +@search_provider(ref_names=['build']) class BuildList( DataExportViewMixin, BuildMixin, diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index be97907e83..e7a305ec53 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -1883,11 +1883,11 @@ class BuildLineTests(BuildAPITest): self.assertEqual(len(response.data), BuildLine.objects.count()) # Filter by 'available' status - response = self.get(url, data={'available': True}, max_query_time=10) + response = self.get(url, data={'available': True}, max_query_time=11) n_t = len(response.data) self.assertGreater(n_t, 0) - response = self.get(url, data={'available': False}, max_query_time=10) + response = self.get(url, data={'available': False}, max_query_time=11) n_f = len(response.data) self.assertGreater(n_f, 0) @@ -2212,14 +2212,14 @@ class BuildLineTests(BuildAPITest): StockItem.objects.create(part=line.bom_item.sub_part, quantity=60) response = self.get( - url, {'build': build.pk, 'available': True}, max_query_time=10 + url, {'build': build.pk, 'available': True}, max_query_time=11 ) # We expect 2 lines to have "available" stock self.assertEqual(len(response.data), 2) response = self.get( - url, {'build': build.pk, 'available': False}, max_query_time=10 + url, {'build': build.pk, 'available': False}, max_query_time=11 ) self.assertEqual(len(response.data), 1) diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index 5c83069342..e7cb91dd5e 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -10,6 +10,7 @@ from django_filters.rest_framework.filterset import FilterSet import common.filters import part.models from data_exporter.mixins import DataExportViewMixin +from generic.search import search_provider from InvenTree.api import ListCreateDestroyAPIView, ParameterListMixin, meta_path from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration from InvenTree.filters import SEARCH_ORDER_FILTER @@ -71,6 +72,14 @@ class CompanyMixin(OutputOptionsMixin): return queryset +@search_provider( + ref_names=['company', 'supplier', 'manufacturer', 'customer'], + default_filters={ + 'supplier': {'is_supplier': True}, + 'manufacturer': {'is_manufacturer': True}, + 'customer': {'is_customer': True}, + }, +) class CompanyList(CompanyMixin, ParameterListMixin, DataExportViewMixin, ListCreateAPI): """API endpoint for accessing a list of Company objects. @@ -212,6 +221,7 @@ class ManufacturerPartMixin(SerializerContextMixin): return queryset +@search_provider(ref_names=['manufacturerpart']) class ManufacturerPartList( DataExportViewMixin, ManufacturerPartMixin, @@ -387,6 +397,7 @@ class SupplierPartMixin: return queryset +@search_provider(ref_names=['supplierpart']) class SupplierPartList( DataExportViewMixin, SupplierPartMixin, diff --git a/src/backend/InvenTree/generic/search/__init__.py b/src/backend/InvenTree/generic/search/__init__.py new file mode 100644 index 0000000000..512963f244 --- /dev/null +++ b/src/backend/InvenTree/generic/search/__init__.py @@ -0,0 +1,5 @@ +"""Generic search provider registration.""" + +from .func import get_search_filters, get_search_providers, search_provider + +__all__ = ['get_search_filters', 'get_search_providers', 'search_provider'] diff --git a/src/backend/InvenTree/generic/search/func.py b/src/backend/InvenTree/generic/search/func.py new file mode 100644 index 0000000000..cc7de98cf5 --- /dev/null +++ b/src/backend/InvenTree/generic/search/func.py @@ -0,0 +1,32 @@ +"""Support for generic search functionality.""" + +from collections.abc import Callable + +SEARCH_PROVIDERS = {} +SEARCH_FILTERS = {} + + +def search_provider( + ref_names: list[str] | None = None, default_filters: dict | None = None +): + """Register a view class as a provider for one or more search result types.""" + + def decorator(func): + """Register the view class for each configured search result type.""" + filters = default_filters or {} + for name in ref_names or [func.__name__.lower()]: + SEARCH_PROVIDERS[name] = func + SEARCH_FILTERS[name] = filters.get(name, {}) + return func + + return decorator + + +def get_search_providers() -> dict[str, Callable]: + """Return the registered search provider classes.""" + return dict(SEARCH_PROVIDERS) + + +def get_search_filters() -> dict[str, dict]: + """Return default filters keyed by search result type.""" + return dict(SEARCH_FILTERS) diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 1da2e8e83e..b2d75d18dc 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -31,6 +31,7 @@ import company.models import stock.models as stock_models import stock.serializers as stock_serializers from data_exporter.mixins import DataExportViewMixin +from generic.search import search_provider from generic.states.api import FSMTransitionMixin, StatusView from InvenTree.api import ( BulkDeleteMixin, @@ -383,6 +384,7 @@ class PurchaseOrderOutputOptions(OutputConfiguration): OPTIONS = [InvenTreeOutputOption('supplier_detail')] +@search_provider(ref_names=['purchaseorder']) class PurchaseOrderViewSet( SerializerContextMixin, OrderCreateMixin, @@ -905,6 +907,7 @@ class SalesOrderOutputOptions(OutputConfiguration): OPTIONS = [InvenTreeOutputOption('customer_detail')] +@search_provider(ref_names=['salesorder']) class SalesOrderList( SalesOrderMixin, OrderCreateMixin, @@ -1547,6 +1550,7 @@ class SalesOrderShipmentMixin: return queryset +@search_provider(ref_names=['salesordershipment']) class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI): """API list endpoint for SalesOrderShipment model.""" @@ -1695,6 +1699,7 @@ class ReturnOrderOutputOptions(OutputConfiguration): OPTIONS = [InvenTreeOutputOption(flag='customer_detail')] +@search_provider(ref_names=['returnorder']) class ReturnOrderList( ReturnOrderMixin, OrderCreateMixin, diff --git a/src/backend/InvenTree/order/test_sales_order.py b/src/backend/InvenTree/order/test_sales_order.py index 88d1e7ea48..88f3c8e8a1 100644 --- a/src/backend/InvenTree/order/test_sales_order.py +++ b/src/backend/InvenTree/order/test_sales_order.py @@ -621,7 +621,7 @@ class SalesOrderTest(InvenTreeAPITestCase): url, expected_code=200, benchmark=True, - max_query_time=10.0, + max_query_time=11, max_query_count=150, ) diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 28e737a1a6..fd4fab2916 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -16,6 +16,7 @@ import common.filters import common.serializers import part.tasks as part_tasks from data_exporter.mixins import DataExportViewMixin +from generic.search import search_provider from InvenTree.api import ( BulkDeleteMixin, BulkUpdateMixin, @@ -218,6 +219,7 @@ class CategoryOutputOption(OutputConfiguration): OPTIONS = [InvenTreeOutputOption(flag='path_detail')] +@search_provider(ref_names=['partcategory']) class CategoryList( CategoryMixin, BulkUpdateMixin, @@ -1025,6 +1027,7 @@ class PartOutputOptions(OutputConfiguration): ] +@search_provider(ref_names=['part']) class PartList( PartMixin, BulkUpdateMixin, diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 8be813183b..c7be5befea 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -149,7 +149,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): {'items': [category.pk for category in categories], 'parent': parent_b.pk}, expected_code=200, max_query_count=60 * len(categories), - max_query_time=10, # Note: in production this is offloaded to the background worker + max_query_time=11, # Note: in production this is offloaded to the background worker ) for category in categories: diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 804df47e68..a63d16f389 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -28,6 +28,7 @@ import stock.serializers as StockSerializers from build.models import Build from company.models import Company, ManufacturerPart, SupplierPart from data_exporter.mixins import DataExportViewMixin +from generic.search import search_provider from generic.states.api import StatusView from InvenTree.api import ( BulkCreateMixin, @@ -442,6 +443,7 @@ class StockLocationOutputOptions(OutputConfiguration): ] +@search_provider(ref_names=['stocklocation']) class StockLocationList( DataExportViewMixin, BulkUpdateMixin, @@ -1117,6 +1119,7 @@ class StockOutputOptions(OutputConfiguration): ] +@search_provider(ref_names=['stockitem']) class StockList( DataExportViewMixin, BulkUpdateMixin,