Update part filters (#7264)

* Expose filter for "bom_valid" status

* Expose part filter for "starred" status

* Bump API version

* Add simple unit test

* Add unit test for "starred" filtering
This commit is contained in:
Oliver 2024-05-20 20:24:20 +10:00 committed by GitHub
parent 2ebe785a75
commit 5cb61d5ad0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 36 deletions

View File

@ -1,11 +1,15 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 198 INVENTREE_API_VERSION = 199
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v199 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264
- Expose "bom_valid" filter for the Part API
- Expose "starred" filter for the Part API
v198 - 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258 v198 - 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258
- Fixed lookup field conflicts in the plugins API - Fixed lookup field conflicts in the plugins API

View File

@ -1106,6 +1106,42 @@ class PartFilter(rest_filters.FilterSet):
label='Default Location', queryset=StockLocation.objects.all() label='Default Location', queryset=StockLocation.objects.all()
) )
bom_valid = rest_filters.BooleanFilter(
label=_('BOM Valid'), method='filter_bom_valid'
)
def filter_bom_valid(self, queryset, name, value):
"""Filter by whether the BOM for the part is valid or not."""
# Limit queryset to active assemblies
queryset = queryset.filter(active=True, assembly=True).distinct()
# Iterate through the queryset
# TODO: We should cache BOM checksums to make this process more efficient
pks = []
for part in queryset:
if part.is_bom_valid() == value:
pks.append(part.pk)
return queryset.filter(pk__in=pks)
starred = rest_filters.BooleanFilter(label='Starred', method='filter_starred')
def filter_starred(self, queryset, name, value):
"""Filter by whether the Part is 'starred' by the current user."""
if self.request.user.is_anonymous:
return queryset
starred_parts = [
star.part.pk
for star in self.request.user.starred_parts.all().prefetch_related('part')
]
if value:
return queryset.filter(pk__in=starred_parts)
else:
return queryset.exclude(pk__in=starred_parts)
is_template = rest_filters.BooleanFilter() is_template = rest_filters.BooleanFilter()
assembly = rest_filters.BooleanFilter() assembly = rest_filters.BooleanFilter()
@ -1235,26 +1271,6 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
queryset = queryset.exclude(pk__in=id_values) queryset = queryset.exclude(pk__in=id_values)
# Filter by whether the BOM has been validated (or not)
bom_valid = params.get('bom_valid', None)
# TODO: Querying bom_valid status may be quite expensive
# TODO: (It needs to be profiled!)
# TODO: It might be worth caching the bom_valid status to a database column
if bom_valid is not None:
bom_valid = str2bool(bom_valid)
# Limit queryset to active assemblies
queryset = queryset.filter(active=True, assembly=True)
pks = []
for prt in queryset:
if prt.is_bom_valid() == bom_valid:
pks.append(prt.pk)
queryset = queryset.filter(pk__in=pks)
# Filter by 'related' parts? # Filter by 'related' parts?
related = params.get('related', None) related = params.get('related', None)
exclude_related = params.get('exclude_related', None) exclude_related = params.get('exclude_related', None)
@ -1288,20 +1304,6 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass pass
# Filter by 'starred' parts?
starred = params.get('starred', None)
if starred is not None:
starred = str2bool(starred)
starred_parts = [
star.part.pk for star in self.request.user.starred_parts.all()
]
if starred:
queryset = queryset.filter(pk__in=starred_parts)
else:
queryset = queryset.exclude(pk__in=starred_parts)
# Cascade? (Default = True) # Cascade? (Default = True)
cascade = str2bool(params.get('cascade', True)) cascade = str2bool(params.get('cascade', True))

View File

@ -281,7 +281,7 @@ class PartCategory(InvenTree.models.InvenTreeTree):
"""Returns True if the specified user subscribes to this category.""" """Returns True if the specified user subscribes to this category."""
return user in self.get_subscribers(**kwargs) return user in self.get_subscribers(**kwargs)
def set_starred(self, user, status): def set_starred(self, user, status: bool) -> None:
"""Set the "subscription" status of this PartCategory against the specified user.""" """Set the "subscription" status of this PartCategory against the specified user."""
if not user: if not user:
return return

View File

@ -747,6 +747,50 @@ class PartAPITest(PartAPITestBase):
response = self.get(url, {'related': 1}, expected_code=200) response = self.get(url, {'related': 1}, expected_code=200)
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 2)
def test_filter_by_bom_valid(self):
"""Test the 'bom_valid' Part API filter."""
url = reverse('api-part-list')
n = Part.objects.filter(active=True, assembly=True).count()
# Initially, there are no parts with a valid BOM
response = self.get(url, {'bom_valid': False}, expected_code=200)
n1 = len(response.data)
for item in response.data:
self.assertTrue(item['assembly'])
self.assertTrue(item['active'])
response = self.get(url, {'bom_valid': True}, expected_code=200)
n2 = len(response.data)
self.assertEqual(n1 + n2, n)
def test_filter_by_starred(self):
"""Test by 'starred' filter."""
url = reverse('api-part-list')
# All parts
n = Part.objects.count()
# Initially, there are no starred parts
response = self.get(url, {'starred': True}, expected_code=200)
self.assertEqual(len(response.data), 0)
response = self.get(url, {'starred': False, 'limit': 1}, expected_code=200)
self.assertEqual(response.data['count'], n)
# Star a part
part = Part.objects.first()
part.set_starred(self.user, True)
# Fetch data again
response = self.get(url, {'starred': True}, expected_code=200)
self.assertEqual(len(response.data), 1)
response = self.get(url, {'starred': False, 'limit': 1}, expected_code=200)
self.assertEqual(response.data['count'], n - 1)
def test_filter_by_convert(self): def test_filter_by_convert(self):
"""Test that we can correctly filter the Part list by conversion options.""" """Test that we can correctly filter the Part list by conversion options."""
category = PartCategory.objects.get(pk=3) category = PartCategory.objects.get(pk=3)