mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-17 01:38:19 +00:00
Bulk add test results (#10146)
* Bulk creation of test results - Add BulkCreateMixin class - Add frontend support * Refactor test result serializer - Allow lookup by template name * Updated unit test * Add unit tests * Add row actions * Docs * Fix failing tests * Bump API version * Fix playwright tests
This commit is contained in:
@@ -474,6 +474,35 @@ class BulkOperationMixin:
|
||||
return queryset
|
||||
|
||||
|
||||
class BulkCreateMixin:
|
||||
"""Mixin class for enabling 'bulk create' operations for various models.
|
||||
|
||||
Bulk create allows for multiple items to be created in a single API query,
|
||||
rather than using multiple API calls to same endpoint.
|
||||
"""
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Perform a POST operation against this list endpoint."""
|
||||
data = request.data
|
||||
|
||||
if isinstance(data, list):
|
||||
created_items = []
|
||||
|
||||
# If data is a list, we assume it is a bulk create request
|
||||
if len(data) == 0:
|
||||
raise ValidationError({'non_field_errors': _('No data provided')})
|
||||
|
||||
for item in data:
|
||||
serializer = self.get_serializer(data=item)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
created_items.append(serializer.data)
|
||||
|
||||
return Response(created_items, status=201)
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BulkUpdateMixin(BulkOperationMixin):
|
||||
"""Mixin class for enabling 'bulk update' operations for various models.
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 381
|
||||
INVENTREE_API_VERSION = 382
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v382 -> 2025-08-07 : https://github.com/inventree/InvenTree/pull/10146
|
||||
- Adds ability to "bulk create" test results via the API
|
||||
- Removes legacy functionality to auto-create test result templates based on provided test names
|
||||
|
||||
v381 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10132
|
||||
- Refactor the "return stock item" API endpoint to align with other stock adjustment actions
|
||||
|
||||
|
||||
@@ -1124,12 +1124,4 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'TEST_UPLOAD_CREATE_TEMPLATE': {
|
||||
'name': _('Create Template on Upload'),
|
||||
'description': _(
|
||||
'Create a new test template when uploading test data which does not match an existing template'
|
||||
),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@ from company.models import Company, SupplierPart
|
||||
from company.serializers import CompanySerializer
|
||||
from data_exporter.mixins import DataExportViewMixin
|
||||
from generic.states.api import StatusView
|
||||
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
|
||||
from InvenTree.api import (
|
||||
BulkCreateMixin,
|
||||
BulkUpdateMixin,
|
||||
ListCreateDestroyAPIView,
|
||||
MetadataView,
|
||||
)
|
||||
from InvenTree.filters import (
|
||||
ORDER_FILTER_ALIAS,
|
||||
SEARCH_ORDER_FILTER,
|
||||
@@ -1360,7 +1365,9 @@ class StockItemTestResultFilter(rest_filters.FilterSet):
|
||||
return queryset.filter(template__key=key)
|
||||
|
||||
|
||||
class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView):
|
||||
class StockItemTestResultList(
|
||||
BulkCreateMixin, StockItemTestResultMixin, ListCreateDestroyAPIView
|
||||
):
|
||||
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
||||
|
||||
filterset_class = StockItemTestResultFilter
|
||||
|
||||
@@ -263,15 +263,18 @@ class StockItemTestResultSerializer(
|
||||
"""Validate the test result data."""
|
||||
stock_item = data['stock_item']
|
||||
template = data.get('template', None)
|
||||
|
||||
# To support legacy API, we can accept a test name instead of a template
|
||||
# In such a case, we use the test name to lookup the appropriate template
|
||||
test_name = self.context['request'].data.get('test', None)
|
||||
|
||||
if not template and not test_name:
|
||||
raise ValidationError(_('Template ID or test name must be provided'))
|
||||
test_name = None
|
||||
|
||||
if not template:
|
||||
# To support legacy API, we can accept a test name instead of a template
|
||||
# In such a case, we use the test name to lookup the appropriate template
|
||||
request_data = self.context['request'].data
|
||||
|
||||
if type(request_data) is list and len(request_data) > 0:
|
||||
request_data = request_data[0]
|
||||
|
||||
test_name = request_data.get('test', test_name)
|
||||
|
||||
test_key = InvenTree.helpers.generateTestKey(test_name)
|
||||
|
||||
ancestors = stock_item.part.get_ancestors(include_self=True)
|
||||
@@ -282,16 +285,8 @@ class StockItemTestResultSerializer(
|
||||
).first():
|
||||
data['template'] = template
|
||||
|
||||
elif get_global_setting('TEST_UPLOAD_CREATE_TEMPLATE', False):
|
||||
logger.debug(
|
||||
"No matching test template found for '%s' - creating a new template",
|
||||
test_name,
|
||||
)
|
||||
|
||||
# Create a new test template based on the provided data
|
||||
data['template'] = part_models.PartTestTemplate.objects.create(
|
||||
part=stock_item.part, test_name=test_name
|
||||
)
|
||||
if not template:
|
||||
raise ValidationError(_('Template ID or test name must be provided'))
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
|
||||
@@ -1882,56 +1882,43 @@ class StockTestResultTest(StockAPITestCase):
|
||||
|
||||
self.post(url, data={'test': 'A test', 'result': True}, expected_code=400)
|
||||
|
||||
# This one should pass!
|
||||
# This one should fail (no matching test template)
|
||||
self.post(
|
||||
url,
|
||||
data={'test': 'A test', 'stock_item': 105, 'result': True},
|
||||
expected_code=201,
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
def test_post(self):
|
||||
"""Test creation of a new test result."""
|
||||
url = self.get_url()
|
||||
|
||||
response = self.client.get(url)
|
||||
n = len(response.data)
|
||||
item = StockItem.objects.get(pk=105)
|
||||
part = item.part
|
||||
|
||||
# Create a new test template for this part
|
||||
test_template = PartTestTemplate.objects.create(
|
||||
part=part,
|
||||
test_name='Checked Steam Valve',
|
||||
description='Test to check the steam valve pressure',
|
||||
)
|
||||
|
||||
# Test upload using test name (legacy method)
|
||||
# Note that a new test template will be created
|
||||
data = {
|
||||
'stock_item': 105,
|
||||
'test': 'Checked Steam Valve',
|
||||
'test': 'checkedsteamvalve',
|
||||
'result': False,
|
||||
'value': '150kPa',
|
||||
'notes': 'I guess there was just too much pressure?',
|
||||
}
|
||||
|
||||
# First, test with TEST_UPLOAD_CREATE_TEMPLATE set to False
|
||||
InvenTreeSetting.set_setting('TEST_UPLOAD_CREATE_TEMPLATE', False, self.user)
|
||||
data = self.post(url, data, expected_code=201).data
|
||||
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertEqual(data['result'], False)
|
||||
self.assertEqual(data['stock_item'], 105)
|
||||
self.assertEqual(data['template'], test_template.pk)
|
||||
|
||||
# Again, with the setting enabled
|
||||
InvenTreeSetting.set_setting('TEST_UPLOAD_CREATE_TEMPLATE', True, self.user)
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
|
||||
# Check that a new test template has been created
|
||||
test_template = PartTestTemplate.objects.get(key='checkedsteamvalve')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(len(response.data), n + 1)
|
||||
|
||||
# And read out again
|
||||
response = self.client.get(url, data={'test': 'Checked Steam Valve'})
|
||||
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
test = response.data[0]
|
||||
self.assertEqual(test['value'], '150kPa')
|
||||
self.assertEqual(test['user'], self.user.pk)
|
||||
|
||||
# Test upload using template reference (new method)
|
||||
# Test upload using template reference
|
||||
data = {
|
||||
'stock_item': 105,
|
||||
'template': test_template.pk,
|
||||
@@ -1941,7 +1928,7 @@ class StockTestResultTest(StockAPITestCase):
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
|
||||
# Check that a new test template has been created
|
||||
# Check that a new test result has been created
|
||||
self.assertEqual(test_template.test_results.all().count(), 2)
|
||||
|
||||
# List test results against the template
|
||||
@@ -1952,6 +1939,43 @@ class StockTestResultTest(StockAPITestCase):
|
||||
for item in response.data:
|
||||
self.assertEqual(item['template'], test_template.pk)
|
||||
|
||||
def test_bulk_create(self):
|
||||
"""Test bulk creation of test results against the API."""
|
||||
url = self.get_url()
|
||||
|
||||
test_template = PartTestTemplate.objects.get(pk=9)
|
||||
part = test_template.part
|
||||
|
||||
N = test_template.test_results.count()
|
||||
|
||||
location = StockLocation.objects.filter(structural=False).first()
|
||||
|
||||
stock_items = [
|
||||
StockItem.objects.create(part=part, quantity=1, location=location)
|
||||
for _ in range(10)
|
||||
]
|
||||
|
||||
# Generate data to bulk-create test results
|
||||
test_data = [
|
||||
{
|
||||
'stock_item': item.pk,
|
||||
'template': test_template.pk,
|
||||
'result': True,
|
||||
'value': f'Test value: {item.pk}',
|
||||
}
|
||||
for item in stock_items
|
||||
]
|
||||
|
||||
data = self.post(url, data=test_data, expected_code=201).data
|
||||
|
||||
self.assertEqual(len(data), 10)
|
||||
self.assertEqual(test_template.test_results.count(), N + 10)
|
||||
|
||||
for item in data:
|
||||
item_id = item['stock_item']
|
||||
self.assertEqual(item['template'], test_template.pk)
|
||||
self.assertEqual(item['value'], f'Test value: {item_id}')
|
||||
|
||||
def test_post_bitmap(self):
|
||||
"""2021-08-25.
|
||||
|
||||
@@ -1998,18 +2022,25 @@ class StockTestResultTest(StockAPITestCase):
|
||||
p.testable = True
|
||||
p.save()
|
||||
|
||||
# Create a test template to record test results against
|
||||
test_template = PartTestTemplate.objects.create(
|
||||
part=p, test_name='Test Template', description='A test template for testing'
|
||||
)
|
||||
|
||||
# Create some objects (via the API)
|
||||
for _ii in range(50):
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
'stock_item': stock_item.pk,
|
||||
'test': f'Some test {_ii}',
|
||||
'test': test_template.key,
|
||||
'result': True,
|
||||
'value': 'Test result value',
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['template'], test_template.pk)
|
||||
|
||||
tests.append(response.data['pk'])
|
||||
|
||||
self.assertEqual(StockItemTestResult.objects.count(), n + 50)
|
||||
|
||||
Reference in New Issue
Block a user