2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Param filters 2 (#9749)

* Filter updates

- Split code
- Allow multiple simultaneous filters against a given parameter
- Bug fixes

* Refactoring

* Cleanup

* fix for operator selection

* Backend fix

* Additional filtering options

* Updated documentation

* Impove filtering logic

* Tweak playwright tests

* Remove debug statements

* Tweak for login test
This commit is contained in:
Oliver
2025-06-08 22:03:50 +10:00
committed by GitHub
parent 026904b361
commit 6b261e122d
13 changed files with 392 additions and 181 deletions

View File

@ -15,7 +15,6 @@ from rest_framework.exceptions import PermissionDenied
from taggit.serializers import TagListSerializerField
import common.models as common_models
import common.settings
import common.validators
import generic.states.custom
from importer.registry import register_importer
@ -142,8 +141,9 @@ class GlobalSettingsSerializer(SettingsSerializer):
- It is overridden by an environment variable.
"""
overrides = common.settings.global_setting_overrides()
from common.settings import global_setting_overrides
overrides = global_setting_overrides()
return obj.key in overrides

View File

@ -1358,8 +1358,9 @@ class PartList(PartMixin, BulkUpdateMixin, DataExportViewMixin, ListCreateAPI):
- <value> is the value to filter against.
"""
# Allowed lookup operations for parameter values
operations = '|'.join(['gt', 'lt', 'gte', 'lte'])
regex_pattern = rf'^parameter_(\d+)(_({operations}))?$'
operators = '|'.join(part.filters.PARAMETER_FILTER_OPERATORS)
regex_pattern = rf'^parameter_(\d+)(_({operators}))?$'
for param in self.request.query_params:
result = re.match(regex_pattern, param)
@ -1370,9 +1371,6 @@ class PartList(PartMixin, BulkUpdateMixin, DataExportViewMixin, ListCreateAPI):
template_id = result.group(1)
operator = result.group(3) or ''
if operator:
operator = '__' + operator
value = self.request.query_params.get(param, None)
queryset = part.filters.filter_by_parameter(

View File

@ -326,6 +326,10 @@ def annotate_sub_categories():
)
"""A list of valid operators for filtering part parameters."""
PARAMETER_FILTER_OPERATORS: list[str] = ['gt', 'gte', 'lt', 'lte', 'ne', 'icontains']
def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
"""Filter the given queryset by a given template parameter.
@ -340,6 +344,9 @@ def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
Returns:
A queryset of Part objects filtered by the given parameter
"""
if func and func not in PARAMETER_FILTER_OPERATORS:
raise ValueError(f'Invalid parameter filter function supplied: {func}.')
try:
template = part.models.PartParameterTemplate.objects.get(pk=template_id)
except (ValueError, part.models.PartParameterTemplate.DoesNotExist):
@ -372,27 +379,49 @@ def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
# The value cannot be converted - return an empty queryset
return queryset.none()
# Special handling for the "not equal" operator
if func == 'ne':
invert = True
func = ''
else:
invert = False
# Some filters are only applicable to string values
text_only = any([func in ['icontains'], value_numeric is None])
# Ensure the function starts with a double underscore
if func and not func.startswith('__'):
func = f'__{func}'
# Query for 'numeric' value - this has priority over 'string' value
q1 = Q(**{
data_numeric = {
'parameters__template': template,
'parameters__data_numeric__isnull': False,
f'parameters__data_numeric{func}': value_numeric,
})
}
query_numeric = Q(**data_numeric)
# Query for 'string' value
q2 = Q(**{
data_text = {
'parameters__template': template,
'parameters__data_numeric__isnull': True,
f'parameters__data{func}': str(value),
})
}
if value_numeric is not None:
queryset = queryset.filter(q1 | q2).distinct()
if not text_only:
data_text['parameters__data_numeric__isnull'] = True
query_text = Q(**data_text)
# Combine the queries based on whether we are filtering by text or numeric value
q = query_text if text_only else query_text | query_numeric
# Special handling for the '__ne' (not equal) operator
# In this case, we want the *opposite* of the above queries
if invert:
return queryset.exclude(q).distinct()
else:
# If the value is not numeric, we only filter by the string value
queryset = queryset.filter(q2).distinct()
return queryset
return queryset.filter(q).distinct()
def order_by_parameter(queryset, template_id: int, ascending=True):