diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 81d0e5ba0b..358530faec 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 433 +INVENTREE_API_VERSION = 434 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v434 -> 2025-12-16 : https://github.com/inventree/InvenTree/pull/11021 + - The "tags" fields (on various API endpoints) is now optional, and disabled by default + - To request tags information, add "tags=true" to the API request query parameters + v433 -> 2025-12-16 : https://github.com/inventree/InvenTree/pull/11023 - "substitutes" field on the BomItem API endpoint is now excluded by default - Add "?substitutes=true" query parameter to include substitute parts in BomItem API endpoint(s) diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index e9034344b5..6c21d9b2d6 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -23,7 +23,7 @@ from rest_framework.fields import empty from rest_framework.mixins import ListModelMixin from rest_framework.serializers import DecimalField from rest_framework.utils import model_meta -from taggit.serializers import TaggitSerializer +from taggit.serializers import TaggitSerializer, TagListSerializerField import common.models as common_models import InvenTree.ready @@ -211,6 +211,12 @@ class FilterableSerializerMixin: if getattr(self, '_exporting_data', False): return + # Skip filtering for a write requests - all fields should be present for data creation + if request := self.context.get('request', None): + if method := getattr(request, 'method', None): + if str(method).lower() in ['post', 'put', 'patch']: + return + # Throw out fields which are not requested (either by default or explicitly) for k, v in self.filter_target_values.items(): # See `enable_filter` where` is_filterable and is_filterable_vals are set @@ -253,6 +259,13 @@ class FilterableIntegerField(FilterableSerializerField, serializers.IntegerField """Custom IntegerField which allows filtering.""" +class FilterableTagListField(FilterableSerializerField, TagListSerializerField): + """Custom TagListSerializerField which allows filtering.""" + + class Meta: + """Empty Meta class.""" + + # endregion diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 19e01491a8..b13d5fdf17 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -1238,7 +1238,6 @@ class BuildItemSerializer( filter_name='stock_detail', prefetch_fields=[ 'stock_item', - 'stock_item__tags', 'stock_item__part', 'stock_item__supplier_part', 'stock_item__supplier_part__manufacturer_part', @@ -1257,7 +1256,7 @@ class BuildItemSerializer( allow_null=True, ), True, - prefetch_fields=['stock_item__location', 'stock_item__location__tags'], + prefetch_fields=['stock_item__location'], ) build_detail = enable_filter( @@ -1389,7 +1388,6 @@ class BuildLineSerializer( 'allocations__stock_item__supplier_part', 'allocations__stock_item__supplier_part__manufacturer_part', 'allocations__stock_item__location', - 'allocations__stock_item__tags', ], ) diff --git a/src/backend/InvenTree/common/filters.py b/src/backend/InvenTree/common/filters.py index b75aea3a95..35b59908ed 100644 --- a/src/backend/InvenTree/common/filters.py +++ b/src/backend/InvenTree/common/filters.py @@ -380,3 +380,21 @@ def enable_parameters_filter(): 'parameters_list__template', ], ) + + +def enable_tags_filter(default: bool = False): + """Add an optional 'tags' field to an API serializer. + + Arguments: + default: If True, enable the filter by default. + + If applied, this field will automatically prefetch the 'tags' relationship. + """ + from InvenTree.serializers import FilterableTagListField + + return InvenTree.serializers.enable_filter( + FilterableTagListField(required=False), + default, + filter_name='tags', + prefetch_fields=['tags', 'tagged_items', 'tagged_items__tag'], + ) diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index b69593a559..d2fee07132 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -12,8 +12,8 @@ from error_report.models import Error from flags.state import flag_state from rest_framework import serializers from rest_framework.exceptions import PermissionDenied -from taggit.serializers import TagListSerializerField +import common.filters import common.models as common_models import common.validators import generic.states.custom @@ -612,7 +612,7 @@ class FailedTaskSerializer(InvenTreeModelSerializer): result = serializers.CharField() -class AttachmentSerializer(InvenTreeModelSerializer): +class AttachmentSerializer(FilterableSerializerMixin, InvenTreeModelSerializer): """Serializer class for the Attachment model.""" class Meta: @@ -645,7 +645,7 @@ class AttachmentSerializer(InvenTreeModelSerializer): 'model_type' ].choices = common.validators.attachment_model_options() - tags = TagListSerializerField(required=False) + tags = common.filters.enable_tags_filter() user_detail = UserSerializer(source='upload_user', read_only=True, many=False) diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index f76cc3a7c8..8ec8a4ae14 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -178,7 +178,7 @@ class ManufacturerPartMixin(SerializerContextMixin): """Return annotated queryset for the ManufacturerPart list endpoint.""" queryset = super().get_queryset(*args, **kwargs) - queryset = queryset.prefetch_related('supplier_parts', 'tags') + queryset = queryset.prefetch_related('supplier_parts') return queryset @@ -323,7 +323,7 @@ class SupplierPartOutputOptions(OutputConfiguration): class SupplierPartMixin: """Mixin class for SupplierPart API endpoints.""" - queryset = SupplierPart.objects.all().prefetch_related('tags') + queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer def get_queryset(self, *args, **kwargs): @@ -331,9 +331,7 @@ class SupplierPartMixin: queryset = super().get_queryset(*args, **kwargs) queryset = SupplierPartSerializer.annotate_queryset(queryset) - queryset = queryset.prefetch_related( - 'part', 'part__pricing_data', 'manufacturer_part__tags' - ) + queryset = queryset.prefetch_related('part', 'part__pricing_data') return queryset diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py index 4e83275df7..96934ff277 100644 --- a/src/backend/InvenTree/company/models.py +++ b/src/backend/InvenTree/company/models.py @@ -591,23 +591,6 @@ class ManufacturerPart( return s -class SupplierPartManager(models.Manager): - """Define custom SupplierPart objects manager. - - The main purpose of this manager is to improve database hit as the - SupplierPart model involves A LOT of foreign keys lookups - """ - - def get_queryset(self): - """Prefetch related fields when querying against the SupplierPart model.""" - # Always prefetch related models - return ( - super() - .get_queryset() - .prefetch_related('part', 'supplier', 'manufacturer_part__manufacturer') - ) - - class SupplierPart( InvenTree.models.InvenTreeAttachmentMixin, InvenTree.models.InvenTreeParameterMixin, @@ -647,8 +630,6 @@ class SupplierPart( # This model was moved from the 'Part' app db_table = 'part_supplierpart' - objects = SupplierPartManager() - tags = TaggableManager(blank=True) @staticmethod diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index d9b7782436..d68e3973fd 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from sql_util.utils import SubqueryCount -from taggit.serializers import TagListSerializerField import common.filters import company.filters @@ -260,7 +259,7 @@ class ManufacturerPartSerializer( 'parameters', ] - tags = TagListSerializerField(required=False) + tags = common.filters.enable_tags_filter() parameters = common.filters.enable_parameters_filter() @@ -383,7 +382,7 @@ class SupplierPartSerializer( 'pack_quantity_native', ] - tags = TagListSerializerField(required=False) + tags = common.filters.enable_tags_filter() def __init__(self, *args, **kwargs): """Initialize this serializer with extra detail fields as required.""" @@ -398,10 +397,9 @@ class SupplierPartSerializer( return if brief: - self.fields.pop('tags') - self.fields.pop('available') - self.fields.pop('on_order') - self.fields.pop('availability_updated') + self.fields.pop('available', None) + self.fields.pop('on_order', None) + self.fields.pop('availability_updated', None) # Annotated field showing total in-stock quantity in_stock = serializers.FloatField( diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 0bdc90daa1..e8ca521915 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -563,7 +563,6 @@ class PurchaseOrderLineItemSerializer( 'part__part', 'part__part__pricing_data', 'part__part__default_location', - 'part__tags', 'part__supplier', 'part__manufacturer_part', 'part__manufacturer_part__manufacturer', diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 612fc02654..8e969dea30 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -1060,6 +1060,7 @@ class PartOutputOptions(OutputConfiguration): InvenTreeOutputOption('location_detail'), InvenTreeOutputOption('path_detail'), InvenTreeOutputOption('price_breaks'), + InvenTreeOutputOption('tags'), ] diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 2f2b74961f..6de1d544d0 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -347,29 +347,6 @@ def rename_part_image(instance, filename): return os.path.join(base, fname) -class PartManager(TreeManager): - """Defines a custom object manager for the Part model. - - The main purpose of this manager is to reduce the number of database hits, - as the Part model has a large number of ForeignKey fields! - """ - - def get_queryset(self): - """Perform default prefetch operations when accessing Part model from the database.""" - return ( - super() - .get_queryset() - .prefetch_related( - 'category', - 'pricing_data', - 'category__parent', - 'stock_items', - 'builds', - 'tags', - ) - ) - - class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a ParameterTemplate. @@ -540,7 +517,7 @@ class Part( NODE_PARENT_KEY = 'variant_of' IMAGE_RENAME = rename_part_image - objects = PartManager() + objects = TreeManager() tags = TaggableManager(blank=True) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 125c140ba6..60ce08275a 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -19,7 +19,6 @@ from djmoney.contrib.exchange.models import convert_money from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from sql_util.utils import SubqueryCount -from taggit.serializers import TagListSerializerField import common.currency import common.filters @@ -633,8 +632,6 @@ class PartSerializer( ] read_only_fields = ['barcode_hash', 'creation_date', 'creation_user'] - tags = TagListSerializerField(required=False) - def __init__(self, *args, **kwargs): """Custom initialization method for PartSerializer. @@ -910,6 +907,8 @@ class PartSerializer( parameters = common.filters.enable_parameters_filter() + tags = common.filters.enable_tags_filter() + price_breaks = enable_filter( PartSalePriceSerializer( source='salepricebreaks', many=True, read_only=True, allow_null=True diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 97f52a9d88..6892488a7a 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -1734,8 +1734,6 @@ class PartDetailTests(PartImageTestMixin, PartAPITestBase): # Now, try to set the name to the *same* value # 2021-06-22 this test is to check that the "duplicate part" checks don't do strange things response = self.patch(url, {'name': 'a new better name'}) - - # Try to remove a tag response = self.patch(url, {'tags': ['tag1']}) self.assertEqual(response.data['tags'], ['tag1']) @@ -2051,8 +2049,7 @@ class PartListTests(PartAPITestBase): with CaptureQueriesContext(connection) as ctx: self.get(url, query, expected_code=200) - # No more than 25 database queries - self.assertLess(len(ctx), 25) + self.assertLess(len(ctx), 30) # Test 'category_detail' annotation for b in [False, True]: @@ -2065,8 +2062,7 @@ class PartListTests(PartAPITestBase): if b and result['category'] is not None: self.assertIn('category_detail', result) - # No more than 25 DB queries - self.assertLessEqual(len(ctx), 25) + self.assertLessEqual(len(ctx), 30) def test_price_breaks(self): """Test that price_breaks parameter works correctly and efficiently.""" diff --git a/src/backend/InvenTree/part/test_category.py b/src/backend/InvenTree/part/test_category.py index a5bb5e6a2d..dca64697da 100644 --- a/src/backend/InvenTree/part/test_category.py +++ b/src/backend/InvenTree/part/test_category.py @@ -157,7 +157,7 @@ class CategoryTest(TestCase): def test_parameters(self): """Test that the Category parameters are correctly fetched.""" # Check number of SQL queries to iterate other parameters - with self.assertNumQueries(9): + with self.assertNumQueries(3): # Prefetch: 3 queries (parts, parameters and parameters_template) fasteners = self.fasteners.prefetch_parts_parameters() # Iterate through all parts and parameters diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 5d12cb4032..07fef64c5c 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -100,21 +100,6 @@ class StockLocationType(InvenTree.models.MetadataMixin, models.Model): ) -class StockLocationManager(TreeManager): - """Custom database manager for the StockLocation class. - - StockLocation querysets will automatically select related fields for performance. - """ - - def get_queryset(self): - """Prefetch queryset to optimize db hits. - - - Joins the StockLocationType by default for speedier icon access - """ - # return super().get_queryset().select_related("location_type") - return super().get_queryset() - - class StockLocationReportContext(report.mixins.BaseReportContext): """Report context for the StockLocation model. @@ -151,7 +136,7 @@ class StockLocation( EXTRA_PATH_FIELDS = ['icon'] - objects = StockLocationManager() + objects = TreeManager() class Meta: """Metaclass defines extra model properties.""" diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 9a8111dbc2..f8d76e543a 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -14,9 +14,9 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.serializers import ValidationError from sql_util.utils import SubqueryCount, SubquerySum -from taggit.serializers import TagListSerializerField import build.models +import common.filters import company.models import company.serializers as company_serializers import InvenTree.helpers @@ -496,7 +496,6 @@ class StockItemSerializer( 'belongs_to', 'sales_order', 'consumed_by', - 'tags', ).select_related('part') # Annotate the queryset with the total allocated to sales orders @@ -579,10 +578,8 @@ class StockItemSerializer( False, prefetch_fields=[ 'supplier_part__supplier', - 'supplier_part__manufacturer_part__manufacturer', - 'supplier_part__manufacturer_part__tags', 'supplier_part__purchase_order_line_items', - 'supplier_part__tags', + 'supplier_part__manufacturer_part__manufacturer', ], ) @@ -655,7 +652,7 @@ class StockItemSerializer( source='sales_order.reference', read_only=True, allow_null=True ) - tags = TagListSerializerField(required=False) + tags = common.filters.enable_tags_filter() class SerializeStockItemSerializer(serializers.Serializer): @@ -1196,7 +1193,7 @@ class LocationSerializer( level = serializers.IntegerField(read_only=True) - tags = TagListSerializerField(required=False) + tags = common.filters.enable_tags_filter() path = enable_filter( FilterableListField(