Merge commit '01fb74af254d29c5f58ee10e352c7e2bb13b23dc' into block-notes

This commit is contained in:
Oliver Walters
2026-06-19 05:41:24 +00:00
15 changed files with 413 additions and 90 deletions
+41
View File
@@ -23,6 +23,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
import InvenTree.config
import InvenTree.filters
import InvenTree.permissions
import InvenTree.version
from common.settings import get_global_setting
@@ -963,3 +964,43 @@ def meta_path(model, lookup_field: str = 'pk', lookup_field_ref: str = 'pk'):
lookup_field_ref=lookup_field_ref,
),
)
class TreeMixin:
"""A mixin class for supporting tree-structured data in the API."""
# Any API view which inherits from this mixin must define a 'model_class' attribute
model_class = None
filter_backends = InvenTree.filters.SEARCH_ORDER_FILTER
search_fields = ['name', 'description']
ordering_fields = ['level', 'name', 'subcategories']
ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']}
ordering = ['level']
def filter_queryset(self, queryset):
"""Filter the queryset, and provide extra support for tree-structured data."""
queryset = super().filter_queryset(queryset)
# If a search term is provided, include all ancestors of matched items in the results
if self.request.query_params.get('search', '').strip():
ancestors = self.model_class.objects.get_queryset_ancestors(
queryset, include_self=True
)
queryset = queryset | ancestors
# If a specific ID is provided to "expand_to", include all ancestors and siblings
if expand_to := self.request.query_params.get('expand_to'):
try:
target = self.model_class.objects.get(pk=int(expand_to))
target_ancestors = target.get_ancestors(include_self=True)
queryset = queryset | target_ancestors
# We also want to include the "sibling" nodes of the expanded item
siblings = target.get_siblings(include_self=True)
queryset = queryset | siblings
except (self.model_class.DoesNotExist, ValueError):
pass
return queryset.distinct()
@@ -1,20 +1,21 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 510
INVENTREE_API_VERSION = 512
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
<<<<<<< HEAD
v510 -> 2026-06-18 : https://github.com/inventree/InvenTree/pull/11971
v512 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/11971
- Removes direct "notes" field from any models which previously supported markdown notes
- Adds a generic "Note" model which can be attached to any model type via a generic foreign key relationship
- Allow multiple notes to be attached to a single object, and for notes to be created / edited / deleted via the API
=======
v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204
- Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints
v510 -> 2026-06-18 : https://github.com/inventree/InvenTree/pull/12197
- Require "staff" access permissions for the machine restart API endpoint
>>>>>>> 6285a11a6538d9a4ff86e68b8d4f0af614f607bf
v509 -> 2026-06-17 : https://github.com/inventree/InvenTree/pull/12184
- Adds "completed_row_count_history" and "row_count_history" fields to the DataImportSession model, which store the historic count of completed rows and total rows for a data import session.
+26 -12
View File
@@ -21,6 +21,7 @@ from InvenTree.api import (
BulkUpdateMixin,
ListCreateDestroyAPIView,
ParameterListMixin,
TreeMixin,
meta_path,
)
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
@@ -284,25 +285,38 @@ class CategoryDetail(CategoryMixin, OutputOptionsMixin, CustomRetrieveUpdateDest
)
class CategoryTree(ListAPI):
class CategoryTreeFilter(FilterSet):
"""Custom filterset class for the CategoryTree endpoint."""
class Meta:
"""Metaclass options for this filterset."""
model = PartCategory
fields = ['parent', 'tree_id', 'level']
max_level = rest_filters.NumberFilter(
label=_('Max Level'),
method='filter_max_level',
help_text=_('Limit the depth of the category tree'),
)
def filter_max_level(self, queryset, name, value):
"""Filter by the maximum depth of the category tree."""
return queryset.filter(level__lte=value)
class CategoryTree(TreeMixin, ListAPI):
"""API endpoint for accessing a list of PartCategory objects ready for rendering a tree."""
model_class = PartCategory
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategoryTree
filter_backends = ORDER_FILTER
ordering_fields = ['level', 'name', 'subcategories']
ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']}
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
serializer_class = part_serializers.CategoryTreeSerializer
filterset_class = CategoryTreeFilter
def get_queryset(self, *args, **kwargs):
"""Return an annotated queryset for the CategoryTree endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.CategoryTree.annotate_queryset(queryset)
queryset = part_serializers.CategoryTreeSerializer.annotate_queryset(queryset)
return queryset
+13 -2
View File
@@ -181,14 +181,25 @@ class CategorySerializer(
parameters = common.filters.enable_parameters_filter()
class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
class CategoryTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for PartCategory tree."""
class Meta:
"""Metaclass defining serializer fields."""
model = PartCategory
fields = ['pk', 'name', 'parent', 'icon', 'structural', 'subcategories']
fields = [
'pk',
'name',
'description',
'pathstring',
'parent',
'tree_id',
'level',
'icon',
'structural',
'subcategories',
]
subcategories = serializers.IntegerField(label=_('Subcategories'), read_only=True)
+24 -11
View File
@@ -33,11 +33,11 @@ from InvenTree.api import (
BulkCreateMixin,
BulkUpdateMixin,
ListCreateDestroyAPIView,
TreeMixin,
meta_path,
)
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
ORDER_FILTER,
SEARCH_ORDER_FILTER,
InvenTreeDateFilter,
NumberOrNullFilter,
@@ -455,20 +455,33 @@ class StockLocationDetail(
)
class StockLocationTree(ListAPI):
class LocationTreeFilter(FilterSet):
"""Custom filterset class for the StockLocationTree endpoint."""
class Meta:
"""Metaclass options for this filterset."""
model = StockLocation
fields = ['parent', 'tree_id', 'level']
max_level = rest_filters.NumberFilter(
label=_('Max Level'),
method='filter_max_level',
help_text=_('Limit the depth of the category tree'),
)
def filter_max_level(self, queryset, name, value):
"""Filter by the maximum depth of the category tree."""
return queryset.filter(level__lte=value)
class StockLocationTree(TreeMixin, ListAPI):
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree."""
model_class = StockLocation
queryset = StockLocation.objects.all()
serializer_class = StockSerializers.LocationTreeSerializer
filter_backends = ORDER_FILTER
ordering_fields = ['level', 'name', 'sublocations']
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
ordering_field_aliases = {'level': ['level', 'name'], 'name': ['name', 'level']}
filterset_class = LocationTreeFilter
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for the StockLocationTree endpoint."""
+12 -1
View File
@@ -1159,7 +1159,18 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Metaclass options."""
model = StockLocation
fields = ['pk', 'name', 'parent', 'icon', 'structural', 'sublocations']
fields = [
'pk',
'name',
'description',
'pathstring',
'parent',
'tree_id',
'level',
'icon',
'structural',
'sublocations',
]
sublocations = serializers.IntegerField(label=_('Sublocations'), read_only=True)