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:
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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."""
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user