2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 14:11:37 +00:00

List filters (#10203)

* Add support for 'list' filtering

* Docs

* Add unit tests
This commit is contained in:
Oliver
2025-08-20 10:23:20 +10:00
committed by GitHub
parent b939e39ea2
commit e44008f528
4 changed files with 71 additions and 3 deletions

View File

@@ -100,6 +100,20 @@ If you enter an invalid option for the filter field, an error message will be di
!!! warning "Advanced Users" !!! warning "Advanced Users"
Report filtering is an advanced topic, and requires a little bit of knowledge of the underlying data structure! Report filtering is an advanced topic, and requires a little bit of knowledge of the underlying data structure!
#### List Filtering
To filter a queryset against a list of ID values, you can use the following "list" syntax:
```
item__in=[1,2,3]
```
Note that:
- The list must be enclosed in square brackets `[]`
- The items in the list must be comma-separated
- The key must end with `__in` to indicate that it is a list filter (*note the double underscore*).
### Metadata ### Metadata
A JSON field made available to any [plugins](../plugins/index.md) - but not used by internal code. A JSON field made available to any [plugins](../plugins/index.md) - but not used by internal code.

View File

@@ -4,6 +4,7 @@ import datetime
import hashlib import hashlib
import inspect import inspect
import io import io
import json
import os import os
import os.path import os.path
import re import re
@@ -729,13 +730,14 @@ def extract_serial_numbers(
return serials return serials
def validateFilterString(value, model=None): def validateFilterString(value: str, model=None) -> dict:
"""Validate that a provided filter string looks like a list of comma-separated key=value pairs. """Validate that a provided filter string looks like a list of comma-separated key=value pairs.
These should nominally match to a valid database filter based on the model being filtered. These should nominally match to a valid database filter based on the model being filtered.
e.g. "category=6, IPN=12" e.g. "category=6, IPN=12"
e.g. "part__name=widget" e.g. "part__name=widget"
e.g. "item=[1,2,3], status=active"
The ReportTemplate class uses the filter string to work out which items a given report applies to. The ReportTemplate class uses the filter string to work out which items a given report applies to.
For example, an acceptance test report template might only apply to stock items with a given IPN, For example, an acceptance test report template might only apply to stock items with a given IPN,
@@ -753,7 +755,8 @@ def validateFilterString(value, model=None):
if not value or len(value) == 0: if not value or len(value) == 0:
return results return results
groups = value.split(',') # Split by comma, but ignore commas within square brackets
groups = re.split(r',(?![^\[]*\])', value)
for group in groups: for group in groups:
group = group.strip() group = group.strip()
@@ -771,6 +774,16 @@ def validateFilterString(value, model=None):
if not k or not v: if not k or not v:
raise ValidationError(f'Invalid group: {group}') raise ValidationError(f'Invalid group: {group}')
# Account for 'list' support
if v.startswith('[') and v.endswith(']'):
try:
v = json.loads(v)
except json.JSONDecodeError:
raise ValidationError(f'Invalid list value: {v}')
if not isinstance(v, list):
raise ValidationError(f'Expected a list for key "{k}", got {type(v)}')
results[k] = v results[k] = v
# If a model is provided, verify that the provided filters can be used against it # If a model is provided, verify that the provided filters can be used against it

View File

@@ -349,6 +349,47 @@ class LabelTest(InvenTreeAPITestCase):
self.assertEqual(output.plugin, 'inventreelabel') self.assertEqual(output.plugin, 'inventreelabel')
self.assertTrue(output.output.name.endswith('.pdf')) self.assertTrue(output.output.name.endswith('.pdf'))
def test_filters(self):
"""Test that template filters are correctly validated."""
from django.core.exceptions import ValidationError
from InvenTree.helpers import validateFilterString
invalid = [
'name=widget, category=6, invalid_field=123',
'category__in=[1,',
'foo=bar',
]
valid = [
'name=widget, category=6',
'category__in=[1,2,3]',
'name=widget , id__in = [99, 199 ] ',
'pk__in=[1,2,3], active=True',
'pk__in=[1, 99], category__in=[1,2,3]',
]
template = LabelTemplate.objects.filter(enabled=True, model_type='part').first()
for f in invalid:
with self.assertRaises(ValidationError):
template.filters = f
template.clean()
for f in valid:
template.filters = f
template.clean()
# Test a specific example
example = ' location__in =[1,2 , 3 ] , status= 3 , id__in=[4,5,6] , part__active=False'
result = validateFilterString(example, model=StockItem)
self.assertEqual(result['location__in'], [1, 2, 3])
self.assertEqual(result['status'], '3')
self.assertEqual(result['id__in'], [4, 5, 6])
self.assertEqual(result['part__active'], 'False')
class PrintTestMixins: class PrintTestMixins:
"""Mixin that enables e2e printing tests.""" """Mixin that enables e2e printing tests."""

View File

@@ -13,7 +13,7 @@ def validate_report_model_type(value):
raise ValidationError('Not a valid model type') raise ValidationError('Not a valid model type')
def validate_filters(value, model=None): def validate_filters(value: str, model=None) -> dict:
"""Validate that the provided model filters are valid.""" """Validate that the provided model filters are valid."""
from InvenTree.helpers import validateFilterString from InvenTree.helpers import validateFilterString