2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +00:00

Refactor API filtering for endpoints

- Specify ContentType by ID, model or app label
This commit is contained in:
Oliver Walters
2025-11-24 23:03:31 +00:00
parent ccc3997518
commit b6c38b91e1
6 changed files with 98 additions and 18 deletions

View File

@@ -768,20 +768,31 @@ class ContentTypeField(serializers.ChoiceField):
content_type = None content_type = None
# First, try to resolve the content type via direct pk value
try: try:
app_label, model = data.split('.') content_type_id = int(data)
content_types = ContentType.objects.filter(app_label=app_label, model=model) content_type = ContentType.objects.get_for_id(content_type_id)
except (ValueError, ContentType.DoesNotExist):
content_type = None
if content_types.exists() and content_types.count() == 1: try:
if len(data.split('.')) == 2:
app_label, model = data.split('.')
content_types = ContentType.objects.filter(
app_label=app_label, model=model
)
if content_types.count() == 1:
# Try exact match first # Try exact match first
content_type = content_types.first() content_type = content_types.first()
else: else:
# Try lookup just on model name # Try lookup just on model name
content_types = ContentType.objects.filter(model=model) content_types = ContentType.objects.filter(model=data)
if content_types.exists() and content_types.count() == 1: if content_types.exists() and content_types.count() == 1:
content_type = content_types.first() content_type = content_types.first()
except Exception: except Exception as e:
print('e:', e)
raise ValidationError(_('Invalid content type format')) raise ValidationError(_('Invalid content type format'))
if content_type is None: if content_type is None:

View File

@@ -28,6 +28,7 @@ from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
import common.filters
import common.models import common.models
import common.serializers import common.serializers
import InvenTree.conversion import InvenTree.conversion
@@ -777,7 +778,7 @@ class ParameterTemplateFilter(FilterSet):
"""Metaclass options.""" """Metaclass options."""
model = common.models.ParameterTemplate model = common.models.ParameterTemplate
fields = ['model_type', 'name', 'units', 'checkbox', 'enabled'] fields = ['name', 'units', 'checkbox', 'enabled']
has_choices = rest_filters.BooleanFilter( has_choices = rest_filters.BooleanFilter(
method='filter_has_choices', label='Has Choice' method='filter_has_choices', label='Has Choice'
@@ -799,6 +800,14 @@ class ParameterTemplateFilter(FilterSet):
return queryset.filter(Q(units=None) | Q(units='')).distinct() return queryset.filter(Q(units=None) | Q(units='')).distinct()
model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')
def filter_model_type(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates of the given model type."""
return common.filters.filter_content_type(
queryset, 'model_type', value, allow_null=False
)
for_model = rest_filters.CharFilter(method='filter_for_model', label='For Model') for_model = rest_filters.CharFilter(method='filter_for_model', label='For Model')
def filter_for_model(self, queryset, name, value): def filter_for_model(self, queryset, name, value):
@@ -807,9 +816,9 @@ class ParameterTemplateFilter(FilterSet):
Note that this varies from the 'model_type' filter, in that ParameterTemplates Note that this varies from the 'model_type' filter, in that ParameterTemplates
with a blank 'model_type' are considered to apply to all models. with a blank 'model_type' are considered to apply to all models.
""" """
return queryset.filter( return common.filters.filter_content_type(
Q(model_type__iexact=value) | Q(model_type__isnull=True) | Q(model_type='') queryset, 'model_type', value, allow_null=True
).distinct() )
class ParameterTemplateMixin: class ParameterTemplateMixin:
@@ -839,12 +848,20 @@ class ParameterFilter(FilterSet):
"""Metaclass options for the filterset.""" """Metaclass options for the filterset."""
model = common.models.Parameter model = common.models.Parameter
fields = ['model_type', 'model_id', 'template', 'updated_by'] fields = ['model_id', 'template', 'updated_by']
enabled = rest_filters.BooleanFilter( enabled = rest_filters.BooleanFilter(
label='Template Enabled', field_name='template__enabled' label='Template Enabled', field_name='template__enabled'
) )
model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')
def filter_model_type(self, queryset, name, value):
"""Filter queryset to include only Parameters of the given model type."""
return common.filters.filter_content_type(
queryset, 'model_type', value, allow_null=False
)
class ParameterMixin: class ParameterMixin:
"""Mixin class for Parameter views.""" """Mixin class for Parameter views."""

View File

@@ -0,0 +1,53 @@
"""Custom API filters for InvenTree."""
def filter_content_type(
queryset, field_name: str, content_type: str | int | None, allow_null: bool = True
):
"""Filter a queryset by content type.
Arguments:
queryset: The queryset to filter.
field_name: The name of the content type field within the current model context.
content_type: The content type to filter by (name or ID).
allow_null: If True, include entries with null content type.
Returns:
Filtered queryset.
"""
if content_type is None:
return queryset
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
ct = None
# First, try to resolve the content type via a PK value
try:
content_type_id = int(content_type)
ct = ContentType.objects.get_for_id(content_type_id)
except (ValueError, ContentType.DoesNotExist):
ct = None
if len(content_type.split('.')) == 2:
# Next, try to resolve the content type via app_label.model_name
try:
app_label, model = content_type.split('.')
ct = ContentType.objects.get(app_label=app_label, model=model)
except ContentType.DoesNotExist:
ct = None
else:
# Next, try to resolve the content type via a model name
ct = ContentType.objects.filter(model__iexact=content_type).first()
if ct is None:
raise ValueError(f'Invalid content type: {content_type}')
q = Q(**{f'{field_name}': ct})
if allow_null:
q |= Q(**{f'{field_name}__isnull': True})
return queryset.filter(q)

View File

@@ -2643,9 +2643,7 @@ class Parameter(
"""Check if the user has the required permission for this parameter.""" """Check if the user has the required permission for this parameter."""
from InvenTree.models import InvenTreeParameterMixin from InvenTree.models import InvenTreeParameterMixin
model_class = common.validators.parameter_model_class_from_label( model_class = self.model_type.model_class()
self.model_type
)
if not issubclass(model_class, InvenTreeParameterMixin): if not issubclass(model_class, InvenTreeParameterMixin):
raise ValidationError(_('Invalid model type specified for parameter')) raise ValidationError(_('Invalid model type specified for parameter'))

View File

@@ -772,9 +772,7 @@ class ParameterSerializer(
# Ensure that the user has permission to modify parameters for the specified model # Ensure that the user has permission to modify parameters for the specified model
user = self.context.get('request').user user = self.context.get('request').user
target_model_class = common.validators.parameter_model_class_from_label( target_model_class = model_type.model_class()
model_type
)
if not issubclass(target_model_class, InvenTreeParameterMixin): if not issubclass(target_model_class, InvenTreeParameterMixin):
raise PermissionDenied(_('Invalid model type specified for parameter')) raise PermissionDenied(_('Invalid model type specified for parameter'))

View File

@@ -161,6 +161,9 @@ export function ParameterTable({
url: ApiEndpoints.parameter_list, url: ApiEndpoints.parameter_list,
title: t`Add Parameter`, title: t`Add Parameter`,
fields: useParameterFields({ modelType, modelId }), fields: useParameterFields({ modelType, modelId }),
initialData: {
data: ''
},
table: table table: table
}); });