mirror of
https://github.com/inventree/InvenTree.git
synced 2026-03-30 08:01:07 +00:00
Improvements for get_bulk_queryset (#11581)
* Improvements for get_bulk_queryset - Limit scope to base view queryset - Remove ability to provide arbitrary filters - Remove feedback if zero items are found * Adjust unit test * Remove filter test * Update CHANGELOG.md
This commit is contained in:
@@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Removed
|
||||
|
||||
- [#11581](https://github.com/inventree/InvenTree/pull/11581) removes the ability to specify arbitrary filters when performing bulk operations via the API. This functionality represented a significant security risk, and was not required for any existing use cases. Bulk operations now only work with a provided list of primary keys.
|
||||
|
||||
## 1.2.0 - 2026-02-12
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -423,23 +423,19 @@ class BulkOperationMixin:
|
||||
def get_bulk_queryset(self, request):
|
||||
"""Return a queryset based on the selection made in the request.
|
||||
|
||||
Selection can be made by providing either:
|
||||
|
||||
- items: A list of primary key values
|
||||
- filters: A dictionary of filter values
|
||||
Selection can be made by providing a list of primary key values,
|
||||
which will be used to filter the queryset.
|
||||
"""
|
||||
model = self.serializer_class.Meta.model
|
||||
|
||||
items = request.data.pop('items', None)
|
||||
filters = request.data.pop('filters', None)
|
||||
all_filter = request.GET.get('all', None)
|
||||
|
||||
queryset = model.objects.all()
|
||||
# Return the base queryset for this model
|
||||
queryset = self.get_queryset()
|
||||
|
||||
if not items and not filters and all_filter is None:
|
||||
if not items and all_filter is None:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _(
|
||||
'List of items or filters must be provided for bulk operation'
|
||||
'List of items must be provided for bulk operation'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -457,19 +453,6 @@ class BulkOperationMixin:
|
||||
'non_field_errors': _('Invalid items list provided')
|
||||
})
|
||||
|
||||
if filters:
|
||||
if type(filters) is not dict:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Filters must be provided as a dict')
|
||||
})
|
||||
|
||||
try:
|
||||
queryset = queryset.filter(**filters)
|
||||
except Exception:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Invalid filters provided')
|
||||
})
|
||||
|
||||
if all_filter and not helpers.str2bool(all_filter):
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('All filter must only be used with true')
|
||||
|
||||
@@ -278,8 +278,7 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
||||
response = self.delete(url, {}, expected_code=400)
|
||||
|
||||
self.assertIn(
|
||||
'List of items or filters must be provided for bulk operation',
|
||||
str(response.data),
|
||||
'List of items must be provided for bulk operation', str(response.data)
|
||||
)
|
||||
|
||||
# DELETE with invalid 'items'
|
||||
@@ -287,11 +286,6 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
||||
|
||||
self.assertIn('Items must be provided as a list', str(response.data))
|
||||
|
||||
# DELETE with invalid 'filters'
|
||||
response = self.delete(url, {'filters': [1, 2, 3]}, expected_code=400)
|
||||
|
||||
self.assertIn('Filters must be provided as a dict', str(response.data))
|
||||
|
||||
|
||||
class SearchTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for global search endpoint."""
|
||||
|
||||
@@ -1352,12 +1352,18 @@ class NotificationTest(InvenTreeAPITestCase):
|
||||
|
||||
# Now, let's bulk delete all 'unread' notifications via the API,
|
||||
# but only associated with the logged in user
|
||||
response = self.delete(url, {'filters': {'read': False}}, expected_code=200)
|
||||
read_notifications = NotificationMessage.objects.filter(read=True)
|
||||
response = self.delete(
|
||||
url, {'items': [ntf.pk for ntf in read_notifications]}, expected_code=200
|
||||
)
|
||||
|
||||
# Only 7 notifications should have been deleted,
|
||||
# Only 3 notifications should have been deleted,
|
||||
# as the notifications associated with other users must remain untouched
|
||||
self.assertEqual(NotificationMessage.objects.count(), 13)
|
||||
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
|
||||
self.assertEqual(NotificationMessage.objects.count(), 17)
|
||||
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 7)
|
||||
self.assertEqual(
|
||||
NotificationMessage.objects.filter(user=self.user, read=True).count(), 0
|
||||
)
|
||||
|
||||
def test_simple(self):
|
||||
"""Test that a simple notification can be created."""
|
||||
|
||||
@@ -2434,17 +2434,7 @@ class StockTestResultTest(StockAPITestCase):
|
||||
self.delete(url, {}, expected_code=400)
|
||||
|
||||
# Now, let's delete all the newly created items with a single API request
|
||||
# However, we will provide incorrect filters
|
||||
response = self.delete(
|
||||
url, {'items': tests, 'filters': {'stock_item': 10}}, expected_code=400
|
||||
)
|
||||
|
||||
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
||||
|
||||
# Try again, but with the correct filters this time
|
||||
response = self.delete(
|
||||
url, {'items': tests, 'filters': {'stock_item': 1}}, expected_code=200
|
||||
)
|
||||
response = self.delete(url, {'items': tests}, expected_code=200)
|
||||
|
||||
self.assertEqual(StockItemTestResult.objects.count(), n)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user