mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-16 17:28:11 +00:00
Implement generic "filtering" against queryset
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
"""Custom API filters for InvenTree."""
|
"""Custom API filters for InvenTree."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
import InvenTree.conversion
|
||||||
|
import InvenTree.helpers
|
||||||
|
|
||||||
|
|
||||||
def filter_content_type(
|
def filter_content_type(
|
||||||
queryset, field_name: str, content_type: str | int | None, allow_null: bool = True
|
queryset, field_name: str, content_type: str | int | None, allow_null: bool = True
|
||||||
@@ -18,9 +27,6 @@ def filter_content_type(
|
|||||||
if content_type is None:
|
if content_type is None:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
ct = None
|
ct = None
|
||||||
|
|
||||||
# First, try to resolve the content type via a PK value
|
# First, try to resolve the content type via a PK value
|
||||||
@@ -51,3 +57,153 @@ def filter_content_type(
|
|||||||
q |= Q(**{f'{field_name}__isnull': True})
|
q |= Q(**{f'{field_name}__isnull': True})
|
||||||
|
|
||||||
return queryset.filter(q)
|
return queryset.filter(q)
|
||||||
|
|
||||||
|
|
||||||
|
"""A list of valid operators for filtering part parameters."""
|
||||||
|
PARAMETER_FILTER_OPERATORS: list[str] = ['gt', 'gte', 'lt', 'lte', 'ne', 'icontains']
|
||||||
|
|
||||||
|
|
||||||
|
def filter_parameters_by_value(
|
||||||
|
queryset: QuerySet, template_id: int, value: str, func: str = ''
|
||||||
|
) -> QuerySet:
|
||||||
|
"""Filter the Parameter model based on the provided template and value.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
queryset: The initial QuerySet to filter.
|
||||||
|
template_id: The parameter template ID to filter by.
|
||||||
|
value: The value to filter against.
|
||||||
|
func: The filtering function to apply (e.g. 'gt', 'lt', etc).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of Parameter instances which match the given criteria.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Parts which do not have a value for the given parameter are excluded.
|
||||||
|
"""
|
||||||
|
from common.models import ParameterTemplate
|
||||||
|
|
||||||
|
# Ensure that the provided function is valid
|
||||||
|
if func and func not in PARAMETER_FILTER_OPERATORS:
|
||||||
|
raise ValueError(f'Invalid parameter filter function: {func}')
|
||||||
|
|
||||||
|
# Ensure that the template exists
|
||||||
|
try:
|
||||||
|
template = ParameterTemplate.objects.get(pk=template_id)
|
||||||
|
except ParameterTemplate.DoesNotExist:
|
||||||
|
raise ValueError(f'Invalid parameter template ID: {template_id}')
|
||||||
|
|
||||||
|
# Construct a "numeric" value for the filter
|
||||||
|
try:
|
||||||
|
value_numeric = float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
value_numeric = None
|
||||||
|
|
||||||
|
if template.checkbox:
|
||||||
|
# Account for 'boolean' parameter values
|
||||||
|
bool_value = InvenTree.helpers.str2bool(value)
|
||||||
|
value_numeric = 1 if bool_value else 0
|
||||||
|
value = str(bool_value)
|
||||||
|
|
||||||
|
# Boolean filtering is limited to exact matches
|
||||||
|
func = ''
|
||||||
|
|
||||||
|
elif value_numeric is None and template.units:
|
||||||
|
# Convert the raw value to the units of the template parameter
|
||||||
|
try:
|
||||||
|
value_numeric = InvenTree.conversion.convert_physical_value(
|
||||||
|
value, template.units
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# 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
|
||||||
|
data_numeric = {
|
||||||
|
'parameters_list__template': template,
|
||||||
|
'parameters_list__data_numeric__isnull': False,
|
||||||
|
f'parameters_list__data_numeric{func}': value_numeric,
|
||||||
|
}
|
||||||
|
|
||||||
|
query_numeric = Q(**data_numeric)
|
||||||
|
|
||||||
|
# Query for 'string' value
|
||||||
|
data_text = {
|
||||||
|
'parameters_list__template': template,
|
||||||
|
f'parameters_list__data{func}': str(value),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not text_only:
|
||||||
|
data_text['parameters_list__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
|
||||||
|
|
||||||
|
# queryset = Parameter.objects.prefetch_related('template').all()
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
return queryset.filter(q).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
def filter_parametric_data(queryset: QuerySet, parameters: dict[str, str]) -> QuerySet:
|
||||||
|
"""Filter the provided queryset by parametric data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
queryset: The initial queryset to filter.
|
||||||
|
parameters: A dictionary of parameter filters to apply.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered queryset.
|
||||||
|
|
||||||
|
Used to filter returned parts based on their parameter values.
|
||||||
|
|
||||||
|
To filter based on parameter value, supply query parameters like:
|
||||||
|
- parameter_<x>=<value>
|
||||||
|
- parameter_<x>_gt=<value>
|
||||||
|
- parameter_<x>_lte=<value>
|
||||||
|
|
||||||
|
where:
|
||||||
|
- <x> is the ID of the PartParameterTemplate.
|
||||||
|
- <value> is the value to filter against.
|
||||||
|
|
||||||
|
Typically these filters would be provided against via an API request.
|
||||||
|
"""
|
||||||
|
queryset = queryset.prefetch_related('parameters_list', 'parameters_list__template')
|
||||||
|
|
||||||
|
# Allowed lookup operations for parameter values
|
||||||
|
operators = '|'.join(PARAMETER_FILTER_OPERATORS)
|
||||||
|
|
||||||
|
regex_pattern = rf'^parameter_(\d+)(_({operators}))?$'
|
||||||
|
|
||||||
|
for param, value in parameters.items():
|
||||||
|
result = re.match(regex_pattern, param)
|
||||||
|
if not result:
|
||||||
|
continue
|
||||||
|
|
||||||
|
template_id = result.group(1)
|
||||||
|
operator = result.group(3) or ''
|
||||||
|
|
||||||
|
queryset = filter_parameters_by_value(
|
||||||
|
queryset, template_id, value, func=operator
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|||||||
@@ -1079,49 +1079,20 @@ class PartList(
|
|||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
"""Perform custom filtering of the queryset."""
|
"""Perform custom filtering of the queryset."""
|
||||||
|
import common.filters
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
queryset = self.filter_parametric_data(queryset)
|
# Filter by parametric data
|
||||||
|
queryset = common.filters.filter_parametric_data(
|
||||||
|
queryset, self.request.query_params
|
||||||
|
)
|
||||||
|
|
||||||
|
# queryset = self.filter_parametric_data(queryset)
|
||||||
queryset = self.order_by_parameter(queryset)
|
queryset = self.order_by_parameter(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_parametric_data(self, queryset):
|
|
||||||
"""Filter queryset against part parameters.
|
|
||||||
|
|
||||||
Used to filter returned parts based on their parameter values.
|
|
||||||
|
|
||||||
To filter based on parameter value, supply query parameters like:
|
|
||||||
- parameter_<x>=<value>
|
|
||||||
- parameter_<x>_gt=<value>
|
|
||||||
- parameter_<x>_lte=<value>
|
|
||||||
|
|
||||||
where:
|
|
||||||
- <x> is the ID of the PartParameterTemplate.
|
|
||||||
- <value> is the value to filter against.
|
|
||||||
"""
|
|
||||||
# Allowed lookup operations for parameter values
|
|
||||||
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)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
continue
|
|
||||||
|
|
||||||
template_id = result.group(1)
|
|
||||||
operator = result.group(3) or ''
|
|
||||||
|
|
||||||
value = self.request.query_params.get(param, None)
|
|
||||||
|
|
||||||
queryset = part.filters.filter_by_parameter(
|
|
||||||
queryset, template_id, value, func=operator
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def order_by_parameter(self, queryset):
|
def order_by_parameter(self, queryset):
|
||||||
"""Perform queryset ordering based on parameter value.
|
"""Perform queryset ordering based on parameter value.
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ from django.db.models.query import QuerySet
|
|||||||
|
|
||||||
from sql_util.utils import SubquerySum
|
from sql_util.utils import SubquerySum
|
||||||
|
|
||||||
import InvenTree.conversion
|
|
||||||
import InvenTree.helpers
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from build.status_codes import BuildStatusGroups
|
from build.status_codes import BuildStatusGroups
|
||||||
@@ -521,106 +519,6 @@ def annotate_bom_item_can_build(queryset: QuerySet, reference: str = '') -> Quer
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
"""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: QuerySet, template_id: int, value: str, func: str = ''
|
|
||||||
) -> QuerySet:
|
|
||||||
"""Filter the given queryset by a given template parameter.
|
|
||||||
|
|
||||||
Parts which do not have a value for the given parameter are excluded.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
queryset: A queryset of Part objects
|
|
||||||
template_id (int): The ID of the template parameter to filter by
|
|
||||||
value (str): The value of the parameter to filter by
|
|
||||||
func (str): The function to use for the filter (e.g. __gt, __lt, __contains)
|
|
||||||
|
|
||||||
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):
|
|
||||||
# Return queryset unchanged if the template does not exist
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
# Construct a "numeric" value
|
|
||||||
try:
|
|
||||||
value_numeric = float(value)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
value_numeric = None
|
|
||||||
|
|
||||||
if template.checkbox:
|
|
||||||
# Account for 'boolean' parameter values
|
|
||||||
# Convert to "True" or "False" string in this case
|
|
||||||
bool_value = InvenTree.helpers.str2bool(value)
|
|
||||||
value_numeric = 1 if bool_value else 0
|
|
||||||
value = str(bool_value)
|
|
||||||
|
|
||||||
# Boolean filtering is limited to exact matches
|
|
||||||
func = ''
|
|
||||||
|
|
||||||
elif value_numeric is None and template.units:
|
|
||||||
# Convert the raw value to the units of the template parameter
|
|
||||||
try:
|
|
||||||
value_numeric = InvenTree.conversion.convert_physical_value(
|
|
||||||
value, template.units
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# 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
|
|
||||||
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
|
|
||||||
data_text = {
|
|
||||||
'parameters__template': template,
|
|
||||||
f'parameters__data{func}': str(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
|
||||||
return queryset.filter(q).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
def order_by_parameter(
|
def order_by_parameter(
|
||||||
queryset: QuerySet, template_id: int, ascending: bool = True
|
queryset: QuerySet, template_id: int, ascending: bool = True
|
||||||
) -> QuerySet:
|
) -> QuerySet:
|
||||||
|
|||||||
Reference in New Issue
Block a user