2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

Query count test (#7157)

* Enforce query count middleware for testing

* Cache "DISPLAY_FULL_NAMES" setting

- Much better API performance

* Update unit_test.py

- Add default check for max query count

* Rework unit_test.py

- x-django-query-count header does not get passed through testing framework

* Adjust middleware settings

* Fix debug print

* Refactoring unit_test.py

* Adjust defaults

* Increase default query threshold

- We can work to reduce this further

* Remove outdated comment

* Install django-middleware-global-request

- Makes the request object globally available
- Cache plugin information against it

* Cache "plugins_checked" against global request

- reduce number of times we need to recalculate plugin data

* Cache plugin information to the request

- Prevent duplicate reloads if not required

* Simplify caching of settings

* Revert line

* Allow higher default counts for POST requests

* Remove global request middleware

- Better to implement proper global cache

* increase CI query thresholds

* Fix typo

* API updates

* Unit test updates

* Increase default MAX_QUERY_TIME

* Increase max query time for plugin functions

* Cleanup barcode unit tests

* Fix part test

* Update more tests

* Further unit test updates

* Updates for unit test code

* Fix for unit testing framework

* Fix

* Reduce default query time

* Increase time allowance
This commit is contained in:
Oliver 2024-05-29 13:18:33 +10:00 committed by GitHub
parent c196511327
commit fb193cae3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 261 additions and 223 deletions

View File

@ -273,13 +273,14 @@ if DEBUG and get_boolean_setting(
'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False 'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False
): ):
MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware') MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware')
logger.debug('Running with debug_querycount middleware enabled')
QUERYCOUNT = { QUERYCOUNT = {
'THRESHOLDS': { 'THRESHOLDS': {
'MEDIUM': 50, 'MEDIUM': 50,
'HIGH': 200, 'HIGH': 200,
'MIN_TIME_TO_LOG': 0, 'MIN_TIME_TO_LOG': 0.1,
'MIN_QUERY_COUNT_TO_LOG': 0, 'MIN_QUERY_COUNT_TO_LOG': 25,
}, },
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'], 'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
'IGNORE_SQL_PATTERNS': [], 'IGNORE_SQL_PATTERNS': [],

View File

@ -4,6 +4,7 @@ import csv
import io import io
import json import json
import re import re
import time
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
@ -228,10 +229,19 @@ class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase): class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
"""Base class for running InvenTree API tests.""" """Base class for running InvenTree API tests."""
# Default query count threshold value
# TODO: This value should be reduced
MAX_QUERY_COUNT = 250
WARNING_QUERY_THRESHOLD = 100
# Default query time threshold value
# TODO: This value should be reduced
# Note: There is a lot of variability in the query time in unit testing...
MAX_QUERY_TIME = 7.5
@contextmanager @contextmanager
def assertNumQueriesLessThan( def assertNumQueriesLessThan(self, value, using='default', verbose=None, url=None):
self, value, using='default', verbose=False, debug=False
):
"""Context manager to check that the number of queries is less than a certain value. """Context manager to check that the number of queries is less than a certain value.
Example: Example:
@ -242,6 +252,13 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
with CaptureQueriesContext(connections[using]) as context: with CaptureQueriesContext(connections[using]) as context:
yield # your test will be run here yield # your test will be run here
n = len(context.captured_queries)
if url and n >= value:
print(
f'Query count exceeded at {url}: Expected < {value} queries, got {n}'
) # pragma: no cover
if verbose: if verbose:
msg = '\r\n%s' % json.dumps( msg = '\r\n%s' % json.dumps(
context.captured_queries, indent=4 context.captured_queries, indent=4
@ -249,34 +266,29 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
else: else:
msg = None msg = None
n = len(context.captured_queries) if url and n > self.WARNING_QUERY_THRESHOLD:
print(f'Warning: {n} queries executed at {url}')
if debug:
print(
f'Expected less than {value} queries, got {n} queries'
) # pragma: no cover
self.assertLess(n, value, msg=msg) self.assertLess(n, value, msg=msg)
def checkResponse(self, url, method, expected_code, response): def check_response(self, url, response, expected_code=None):
"""Debug output for an unexpected response.""" """Debug output for an unexpected response."""
# No expected code, return # Check that the response returned the expected status code
if expected_code is None:
return
if expected_code != response.status_code: # pragma: no cover if expected_code is not None:
print( if expected_code != response.status_code: # pragma: no cover
f"Unexpected {method} response at '{url}': status_code = {response.status_code}" print(
) f"Unexpected response at '{url}': status_code = {response.status_code} (expected {expected_code})"
)
if hasattr(response, 'data'): if hasattr(response, 'data'):
print('data:', response.data) print('data:', response.data)
if hasattr(response, 'body'): if hasattr(response, 'body'):
print('body:', response.body) print('body:', response.body)
if hasattr(response, 'content'): if hasattr(response, 'content'):
print('content:', response.content) print('content:', response.content)
self.assertEqual(expected_code, response.status_code) self.assertEqual(expected_code, response.status_code)
def getActions(self, url): def getActions(self, url):
"""Return a dict of the 'actions' available at a given endpoint. """Return a dict of the 'actions' available at a given endpoint.
@ -289,72 +301,88 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
actions = response.data.get('actions', {}) actions = response.data.get('actions', {})
return actions return actions
def get(self, url, data=None, expected_code=200, format='json', **kwargs): def query(self, url, method, data=None, **kwargs):
"""Perform a generic API query."""
if data is None:
data = {}
expected_code = kwargs.pop('expected_code', None)
kwargs['format'] = kwargs.get('format', 'json')
max_queries = kwargs.get('max_query_count', self.MAX_QUERY_COUNT)
max_query_time = kwargs.get('max_query_time', self.MAX_QUERY_TIME)
t1 = time.time()
with self.assertNumQueriesLessThan(max_queries, url=url):
response = method(url, data, **kwargs)
t2 = time.time()
dt = t2 - t1
self.check_response(url, response, expected_code=expected_code)
if dt > max_query_time:
print(
f'Query time exceeded at {url}: Expected {max_query_time}s, got {dt}s'
)
self.assertLessEqual(dt, max_query_time)
return response
def get(self, url, data=None, expected_code=200, **kwargs):
"""Issue a GET request.""" """Issue a GET request."""
# Set default - see B006 kwargs['data'] = data
if data is None:
data = {}
response = self.client.get(url, data, format=format, **kwargs) return self.query(url, self.client.get, expected_code=expected_code, **kwargs)
self.checkResponse(url, 'GET', expected_code, response) def post(self, url, data=None, expected_code=201, **kwargs):
return response
def post(self, url, data=None, expected_code=None, format='json', **kwargs):
"""Issue a POST request.""" """Issue a POST request."""
# Set default value - see B006 # Default query limit is higher for POST requests, due to extra event processing
if data is None: kwargs['max_query_count'] = kwargs.get(
data = {} 'max_query_count', self.MAX_QUERY_COUNT + 100
)
response = self.client.post(url, data=data, format=format, **kwargs) kwargs['data'] = data
self.checkResponse(url, 'POST', expected_code, response) return self.query(url, self.client.post, expected_code=expected_code, **kwargs)
return response def delete(self, url, data=None, expected_code=204, **kwargs):
def delete(self, url, data=None, expected_code=None, format='json', **kwargs):
"""Issue a DELETE request.""" """Issue a DELETE request."""
if data is None: kwargs['data'] = data
data = {}
response = self.client.delete(url, data=data, format=format, **kwargs) return self.query(
url, self.client.delete, expected_code=expected_code, **kwargs
)
self.checkResponse(url, 'DELETE', expected_code, response) def patch(self, url, data, expected_code=200, **kwargs):
return response
def patch(self, url, data, expected_code=None, format='json', **kwargs):
"""Issue a PATCH request.""" """Issue a PATCH request."""
response = self.client.patch(url, data=data, format=format, **kwargs) kwargs['data'] = data
self.checkResponse(url, 'PATCH', expected_code, response) return self.query(url, self.client.patch, expected_code=expected_code, **kwargs)
return response def put(self, url, data, expected_code=200, **kwargs):
def put(self, url, data, expected_code=None, format='json', **kwargs):
"""Issue a PUT request.""" """Issue a PUT request."""
response = self.client.put(url, data=data, format=format, **kwargs) kwargs['data'] = data
self.checkResponse(url, 'PUT', expected_code, response) return self.query(url, self.client.put, expected_code=expected_code, **kwargs)
return response
def options(self, url, expected_code=None, **kwargs): def options(self, url, expected_code=None, **kwargs):
"""Issue an OPTIONS request.""" """Issue an OPTIONS request."""
response = self.client.options(url, format='json', **kwargs) kwargs['data'] = kwargs.get('data', None)
self.checkResponse(url, 'OPTIONS', expected_code, response) return self.query(
url, self.client.options, expected_code=expected_code, **kwargs
return response )
def download_file( def download_file(
self, url, data, expected_code=None, expected_fn=None, decode=True self, url, data, expected_code=None, expected_fn=None, decode=True, **kwargs
): ):
"""Download a file from the server, and return an in-memory file.""" """Download a file from the server, and return an in-memory file."""
response = self.client.get(url, data=data, format='json') response = self.client.get(url, data=data, format='json')
self.checkResponse(url, 'DOWNLOAD_FILE', expected_code, response) self.check_response(url, response, expected_code=expected_code)
# Check that the response is of the correct type # Check that the response is of the correct type
if not isinstance(response, StreamingHttpResponse): if not isinstance(response, StreamingHttpResponse):

View File

@ -567,7 +567,7 @@ class Build(
self.allocated_stock.delete() self.allocated_stock.delete()
@transaction.atomic @transaction.atomic
def complete_build(self, user): def complete_build(self, user, trim_allocated_stock=False):
"""Mark this build as complete.""" """Mark this build as complete."""
import build.tasks import build.tasks
@ -575,6 +575,9 @@ class Build(
if self.incomplete_count > 0: if self.incomplete_count > 0:
return return
if trim_allocated_stock:
self.trim_allocated_stock()
self.completion_date = InvenTree.helpers.current_date() self.completion_date = InvenTree.helpers.current_date()
self.completed_by = user self.completed_by = user
self.status = BuildStatus.COMPLETE.value self.status = BuildStatus.COMPLETE.value
@ -858,6 +861,10 @@ class Build(
def trim_allocated_stock(self): def trim_allocated_stock(self):
"""Called after save to reduce allocated stock if the build order is now overallocated.""" """Called after save to reduce allocated stock if the build order is now overallocated."""
# Only need to worry about untracked stock here # Only need to worry about untracked stock here
items_to_save = []
items_to_delete = []
for build_line in self.untracked_line_items: for build_line in self.untracked_line_items:
reduce_by = build_line.allocated_quantity() - build_line.quantity reduce_by = build_line.allocated_quantity() - build_line.quantity
@ -875,13 +882,19 @@ class Build(
# Easy case - this item can just be reduced. # Easy case - this item can just be reduced.
if item.quantity > reduce_by: if item.quantity > reduce_by:
item.quantity -= reduce_by item.quantity -= reduce_by
item.save() items_to_save.append(item)
break break
# Harder case, this item needs to be deleted, and any remainder # Harder case, this item needs to be deleted, and any remainder
# taken from the next items in the list. # taken from the next items in the list.
reduce_by -= item.quantity reduce_by -= item.quantity
item.delete() items_to_delete.append(item)
# Save the updated BuildItem objects
BuildItem.objects.bulk_update(items_to_save, ['quantity'])
# Delete the remaining BuildItem objects
BuildItem.objects.filter(pk__in=[item.pk for item in items_to_delete]).delete()
@property @property
def allocated_stock(self): def allocated_stock(self):
@ -978,7 +991,10 @@ class Build(
# List the allocated BuildItem objects for the given output # List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all() allocated_items = output.items_to_install.all()
if (common.settings.prevent_build_output_complete_on_incompleted_tests() and output.hasRequiredTests() and not output.passedAllRequiredTests()): required_tests = kwargs.get('required_tests', output.part.getRequiredTests())
prevent_on_incomplete = kwargs.get('prevent_on_incomplete', common.settings.prevent_build_output_complete_on_incompleted_tests())
if (prevent_on_incomplete and not output.passedAllRequiredTests(required_tests=required_tests)):
serial = output.serial serial = output.serial
raise ValidationError( raise ValidationError(
_(f"Build output {serial} has not passed all required tests")) _(f"Build output {serial} has not passed all required tests"))

View File

@ -568,10 +568,13 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
outputs = data.get('outputs', []) outputs = data.get('outputs', [])
# Cache some calculated values which can be passed to each output
required_tests = outputs[0]['output'].part.getRequiredTests()
prevent_on_incomplete = common.settings.prevent_build_output_complete_on_incompleted_tests()
# Mark the specified build outputs as "complete" # Mark the specified build outputs as "complete"
with transaction.atomic(): with transaction.atomic():
for item in outputs: for item in outputs:
output = item['output'] output = item['output']
build.complete_build_output( build.complete_build_output(
@ -580,6 +583,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
location=location, location=location,
status=status, status=status,
notes=notes, notes=notes,
required_tests=required_tests,
prevent_on_incomplete=prevent_on_incomplete,
) )
@ -734,10 +739,11 @@ class BuildCompleteSerializer(serializers.Serializer):
build = self.context['build'] build = self.context['build']
data = self.validated_data data = self.validated_data
if data.get('accept_overallocated', OverallocationChoice.REJECT) == OverallocationChoice.TRIM:
build.trim_allocated_stock()
build.complete_build(request.user) build.complete_build(
request.user,
trim_allocated_stock=data.get('accept_overallocated', OverallocationChoice.REJECT) == OverallocationChoice.TRIM
)
class BuildUnallocationSerializer(serializers.Serializer): class BuildUnallocationSerializer(serializers.Serializer):

View File

@ -223,6 +223,7 @@ class BuildTest(BuildAPITest):
"status": 50, # Item requires attention "status": 50, # Item requires attention
}, },
expected_code=201, expected_code=201,
max_query_count=450, # TODO: Try to optimize this
) )
self.assertEqual(self.build.incomplete_outputs.count(), 0) self.assertEqual(self.build.incomplete_outputs.count(), 0)
@ -992,6 +993,7 @@ class BuildOverallocationTest(BuildAPITest):
'accept_overallocated': 'accept', 'accept_overallocated': 'accept',
}, },
expected_code=201, expected_code=201,
max_query_count=550, # TODO: Come back and refactor this
) )
self.build.refresh_from_db() self.build.refresh_from_db()
@ -1012,6 +1014,7 @@ class BuildOverallocationTest(BuildAPITest):
'accept_overallocated': 'trim', 'accept_overallocated': 'trim',
}, },
expected_code=201, expected_code=201,
max_query_count=550, # TODO: Come back and refactor this
) )
self.build.refresh_from_db() self.build.refresh_from_db()

View File

@ -749,6 +749,7 @@ class BaseInvenTreeSetting(models.Model):
attempts=attempts - 1, attempts=attempts - 1,
**kwargs, **kwargs,
) )
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
logger.warning("Database is locked, cannot set setting '%s'", key) logger.warning("Database is locked, cannot set setting '%s'", key)
# Likely the DB is locked - not much we can do here # Likely the DB is locked - not much we can do here

View File

@ -1093,7 +1093,7 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Updating via the external exchange may not work every time # Updating via the external exchange may not work every time
for _idx in range(5): for _idx in range(5):
self.post(reverse('api-currency-refresh')) self.post(reverse('api-currency-refresh'), expected_code=200)
# There should be some new exchange rate objects now # There should be some new exchange rate objects now
if Rate.objects.all().exists(): if Rate.objects.all().exists():

View File

@ -1108,7 +1108,7 @@ class PurchaseOrderReceiveTest(OrderTest):
n = StockItem.objects.count() n = StockItem.objects.count()
self.post(self.url, data, expected_code=201) self.post(self.url, data, expected_code=201, max_query_count=400)
# Check that the expected number of stock items has been created # Check that the expected number of stock items has been created
self.assertEqual(n + 11, StockItem.objects.count()) self.assertEqual(n + 11, StockItem.objects.count())

View File

@ -371,8 +371,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
params['delete_child_categories'] = '1' params['delete_child_categories'] = '1'
response = self.delete(url, params, expected_code=204) response = self.delete(url, params, expected_code=204)
self.assertEqual(response.status_code, 204)
if delete_parts: if delete_parts:
if i == Target.delete_subcategories_delete_parts: if i == Target.delete_subcategories_delete_parts:
# Check if all parts deleted # Check if all parts deleted
@ -685,7 +683,6 @@ class PartAPITest(PartAPITestBase):
# Request *all* part categories # Request *all* part categories
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 8) self.assertEqual(len(response.data), 8)
# Request top-level part categories only # Request top-level part categories only
@ -709,7 +706,6 @@ class PartAPITest(PartAPITestBase):
url = reverse('api-part-category-list') url = reverse('api-part-category-list')
response = self.post(url, data) response = self.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
parent = response.data['pk'] parent = response.data['pk']
@ -717,7 +713,6 @@ class PartAPITest(PartAPITestBase):
for animal in ['cat', 'dog', 'zebra']: for animal in ['cat', 'dog', 'zebra']:
data = {'name': animal, 'description': 'A sort of animal', 'parent': parent} data = {'name': animal, 'description': 'A sort of animal', 'parent': parent}
response = self.post(url, data) response = self.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['parent'], parent) self.assertEqual(response.data['parent'], parent)
self.assertEqual(response.data['name'], animal) self.assertEqual(response.data['name'], animal)
self.assertEqual(response.data['pathstring'], 'Animals/' + animal) self.assertEqual(response.data['pathstring'], 'Animals/' + animal)
@ -741,7 +736,6 @@ class PartAPITest(PartAPITestBase):
data['parent'] = None data['parent'] = None
data['description'] = 'Changing the description' data['description'] = 'Changing the description'
response = self.patch(url, data) response = self.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['description'], 'Changing the description') self.assertEqual(response.data['description'], 'Changing the description')
self.assertIsNone(response.data['parent']) self.assertIsNone(response.data['parent'])
@ -750,13 +744,11 @@ class PartAPITest(PartAPITestBase):
url = reverse('api-part-list') url = reverse('api-part-list')
data = {'cascade': True} data = {'cascade': True}
response = self.get(url, data) response = self.get(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), Part.objects.count()) self.assertEqual(len(response.data), Part.objects.count())
# Test filtering parts by category # Test filtering parts by category
data = {'category': 2} data = {'category': 2}
response = self.get(url, data) response = self.get(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# There should only be 2 objects in category C # There should only be 2 objects in category C
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 2)
@ -897,7 +889,6 @@ class PartAPITest(PartAPITestBase):
response = self.get(url, data) response = self.get(url, data)
# Now there should be 5 total parts # Now there should be 5 total parts
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 3) self.assertEqual(len(response.data), 3)
def test_test_templates(self): def test_test_templates(self):
@ -906,8 +897,6 @@ class PartAPITest(PartAPITestBase):
# List ALL items # List ALL items
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 9) self.assertEqual(len(response.data), 9)
# Request for a particular part # Request for a particular part
@ -921,10 +910,9 @@ class PartAPITest(PartAPITestBase):
response = self.post( response = self.post(
url, url,
data={'part': 10000, 'test_name': 'My very first test', 'required': False}, data={'part': 10000, 'test_name': 'My very first test', 'required': False},
expected_code=400,
) )
self.assertEqual(response.status_code, 400)
# Try to post a new object (should succeed) # Try to post a new object (should succeed)
response = self.post( response = self.post(
url, url,
@ -936,20 +924,17 @@ class PartAPITest(PartAPITestBase):
}, },
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# Try to post a new test with the same name (should fail) # Try to post a new test with the same name (should fail)
response = self.post( response = self.post(
url, url,
data={'part': 10004, 'test_name': ' newtest', 'description': 'dafsdf'}, data={'part': 10004, 'test_name': ' newtest', 'description': 'dafsdf'},
expected_code=400,
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# Try to post a new test against a non-trackable part (should fail) # Try to post a new test against a non-trackable part (should fail)
response = self.post(url, data={'part': 1, 'test_name': 'A simple test'}) response = self.post(
url, data={'part': 1, 'test_name': 'A simple test'}, expected_code=400
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) )
def test_get_thumbs(self): def test_get_thumbs(self):
"""Return list of part thumbnails.""" """Return list of part thumbnails."""
@ -957,8 +942,6 @@ class PartAPITest(PartAPITestBase):
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_paginate(self): def test_paginate(self):
"""Test pagination of the Part list API.""" """Test pagination of the Part list API."""
for n in [1, 5, 10]: for n in [1, 5, 10]:
@ -1450,8 +1433,6 @@ class PartDetailTests(PartAPITestBase):
}, },
) )
self.assertEqual(response.status_code, 201)
pk = response.data['pk'] pk = response.data['pk']
# Check that a new part has been added # Check that a new part has been added
@ -1470,7 +1451,6 @@ class PartDetailTests(PartAPITestBase):
response = self.patch(url, {'name': 'a new better name'}) response = self.patch(url, {'name': 'a new better name'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['pk'], pk) self.assertEqual(response.data['pk'], pk)
self.assertEqual(response.data['name'], 'a new better name') self.assertEqual(response.data['name'], 'a new better name')
@ -1486,24 +1466,17 @@ class PartDetailTests(PartAPITestBase):
# 2021-06-22 this test is to check that the "duplicate part" checks don't do strange things # 2021-06-22 this test is to check that the "duplicate part" checks don't do strange things
response = self.patch(url, {'name': 'a new better name'}) response = self.patch(url, {'name': 'a new better name'})
self.assertEqual(response.status_code, 200)
# Try to remove a tag # Try to remove a tag
response = self.patch(url, {'tags': ['tag1']}) response = self.patch(url, {'tags': ['tag1']})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['tags'], ['tag1']) self.assertEqual(response.data['tags'], ['tag1'])
# Try to remove the part # Try to remove the part
response = self.delete(url) response = self.delete(url, expected_code=400)
# As the part is 'active' we cannot delete it
self.assertEqual(response.status_code, 400)
# So, let's make it not active # So, let's make it not active
response = self.patch(url, {'active': False}, expected_code=200) response = self.patch(url, {'active': False}, expected_code=200)
response = self.delete(url) response = self.delete(url)
self.assertEqual(response.status_code, 204)
# Part count should have reduced # Part count should have reduced
self.assertEqual(Part.objects.count(), n) self.assertEqual(Part.objects.count(), n)
@ -1522,8 +1495,6 @@ class PartDetailTests(PartAPITestBase):
}, },
) )
self.assertEqual(response.status_code, 201)
n = Part.objects.count() n = Part.objects.count()
# Check that we cannot create a duplicate in a different category # Check that we cannot create a duplicate in a different category
@ -1536,10 +1507,9 @@ class PartDetailTests(PartAPITestBase):
'category': 2, 'category': 2,
'revision': 'A', 'revision': 'A',
}, },
expected_code=400,
) )
self.assertEqual(response.status_code, 400)
# Check that only 1 matching part exists # Check that only 1 matching part exists
parts = Part.objects.filter( parts = Part.objects.filter(
name='part', description='description', IPN='IPN-123' name='part', description='description', IPN='IPN-123'
@ -1560,9 +1530,9 @@ class PartDetailTests(PartAPITestBase):
'category': 2, 'category': 2,
'revision': 'B', 'revision': 'B',
}, },
expected_code=201,
) )
self.assertEqual(response.status_code, 201)
self.assertEqual(Part.objects.count(), n + 1) self.assertEqual(Part.objects.count(), n + 1)
# Now, check that we cannot *change* an existing part to conflict # Now, check that we cannot *change* an existing part to conflict
@ -1571,14 +1541,10 @@ class PartDetailTests(PartAPITestBase):
url = reverse('api-part-detail', kwargs={'pk': pk}) url = reverse('api-part-detail', kwargs={'pk': pk})
# Attempt to alter the revision code # Attempt to alter the revision code
response = self.patch(url, {'revision': 'A'}) response = self.patch(url, {'revision': 'A'}, expected_code=400)
self.assertEqual(response.status_code, 400)
# But we *can* change it to a unique revision code # But we *can* change it to a unique revision code
response = self.patch(url, {'revision': 'C'}) response = self.patch(url, {'revision': 'C'}, expected_code=200)
self.assertEqual(response.status_code, 200)
def test_image_upload(self): def test_image_upload(self):
"""Test that we can upload an image to the part API.""" """Test that we can upload an image to the part API."""
@ -1608,10 +1574,9 @@ class PartDetailTests(PartAPITestBase):
with open(f'{test_path}.txt', 'rb') as dummy_image: with open(f'{test_path}.txt', 'rb') as dummy_image:
response = self.upload_client.patch( response = self.upload_client.patch(
url, {'image': dummy_image}, format='multipart' url, {'image': dummy_image}, format='multipart', expected_code=400
) )
self.assertEqual(response.status_code, 400)
self.assertIn('Upload a valid image', str(response.data)) self.assertIn('Upload a valid image', str(response.data))
# Now try to upload a valid image file, in multiple formats # Now try to upload a valid image file, in multiple formats
@ -1623,11 +1588,9 @@ class PartDetailTests(PartAPITestBase):
with open(fn, 'rb') as dummy_image: with open(fn, 'rb') as dummy_image:
response = self.upload_client.patch( response = self.upload_client.patch(
url, {'image': dummy_image}, format='multipart' url, {'image': dummy_image}, format='multipart', expected_code=200
) )
self.assertEqual(response.status_code, 200)
# And now check that the image has been set # And now check that the image has been set
p = Part.objects.get(pk=pk) p = Part.objects.get(pk=pk)
self.assertIsNotNone(p.image) self.assertIsNotNone(p.image)
@ -1644,10 +1607,11 @@ class PartDetailTests(PartAPITestBase):
with open(fn, 'rb') as img_file: with open(fn, 'rb') as img_file:
response = self.upload_client.patch( response = self.upload_client.patch(
reverse('api-part-detail', kwargs={'pk': p.pk}), {'image': img_file} reverse('api-part-detail', kwargs={'pk': p.pk}),
{'image': img_file},
expected_code=200,
) )
self.assertEqual(response.status_code, 200)
image_name = response.data['image'] image_name = response.data['image']
self.assertTrue(image_name.startswith('/media/part_images/part_image')) self.assertTrue(image_name.startswith('/media/part_images/part_image'))
@ -1690,10 +1654,11 @@ class PartDetailTests(PartAPITestBase):
# Upload the image to a part # Upload the image to a part
with open(fn, 'rb') as img_file: with open(fn, 'rb') as img_file:
response = self.upload_client.patch( response = self.upload_client.patch(
reverse('api-part-detail', kwargs={'pk': p.pk}), {'image': img_file} reverse('api-part-detail', kwargs={'pk': p.pk}),
{'image': img_file},
expected_code=200,
) )
self.assertEqual(response.status_code, 200)
image_name = response.data['image'] image_name = response.data['image']
self.assertTrue(image_name.startswith('/media/part_images/part_image')) self.assertTrue(image_name.startswith('/media/part_images/part_image'))
@ -1949,8 +1914,6 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
response = self.get(url) response = self.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for part in response.data: for part in response.data:
if part['pk'] == self.part.pk: if part['pk'] == self.part.pk:
return part return part
@ -2677,8 +2640,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
InvenTreeSetting.set_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', True) InvenTreeSetting.set_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', True)
response = self.delete(reverse('api-part-detail', kwargs={'pk': 1})) self.delete(reverse('api-part-detail', kwargs={'pk': 1}))
self.assertEqual(response.status_code, 204)
with self.assertRaises(Part.DoesNotExist): with self.assertRaises(Part.DoesNotExist):
p.refresh_from_db() p.refresh_from_db()

View File

@ -27,25 +27,19 @@ class BarcodeAPITest(InvenTreeAPITestCase):
def postBarcode(self, url, barcode, expected_code=None): def postBarcode(self, url, barcode, expected_code=None):
"""Post barcode and return results.""" """Post barcode and return results."""
return self.post( return self.post(
url, url, data={'barcode': str(barcode)}, expected_code=expected_code
format='json',
data={'barcode': str(barcode)},
expected_code=expected_code,
) )
def test_invalid(self): def test_invalid(self):
"""Test that invalid requests fail.""" """Test that invalid requests fail."""
# test scan url # test scan url
self.post(self.scan_url, format='json', data={}, expected_code=400) self.post(self.scan_url, data={}, expected_code=400)
# test wrong assign urls # test wrong assign urls
self.post(self.assign_url, format='json', data={}, expected_code=400) self.post(self.assign_url, data={}, expected_code=400)
self.post( self.post(self.assign_url, data={'barcode': '123'}, expected_code=400)
self.assign_url, format='json', data={'barcode': '123'}, expected_code=400
)
self.post( self.post(
self.assign_url, self.assign_url,
format='json',
data={'barcode': '123', 'stockitem': '123'}, data={'barcode': '123', 'stockitem': '123'},
expected_code=400, expected_code=400,
) )
@ -163,7 +157,6 @@ class BarcodeAPITest(InvenTreeAPITestCase):
response = self.post( response = self.post(
self.assign_url, self.assign_url,
format='json',
data={'barcode': barcode_data, 'stockitem': item.pk}, data={'barcode': barcode_data, 'stockitem': item.pk},
expected_code=200, expected_code=200,
) )
@ -183,7 +176,6 @@ class BarcodeAPITest(InvenTreeAPITestCase):
# Ensure that the same barcode hash cannot be assigned to a different stock item! # Ensure that the same barcode hash cannot be assigned to a different stock item!
response = self.post( response = self.post(
self.assign_url, self.assign_url,
format='json',
data={'barcode': barcode_data, 'stockitem': 521}, data={'barcode': barcode_data, 'stockitem': 521},
expected_code=400, expected_code=400,
) )

View File

@ -23,10 +23,6 @@ def trigger_event(event, *args, **kwargs):
""" """
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
if not settings.PLUGINS_ENABLED:
# Do nothing if plugins are not enabled
return # pragma: no cover
if not InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS', False): if not InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS', False):
# Do nothing if plugin events are not enabled # Do nothing if plugin events are not enabled
return return
@ -59,7 +55,9 @@ def register_event(event, *args, **kwargs):
logger.debug("Registering triggered event: '%s'", event) logger.debug("Registering triggered event: '%s'", event)
# Determine if there are any plugins which are interested in responding # Determine if there are any plugins which are interested in responding
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting(
'ENABLE_PLUGINS_EVENTS', cache=False
):
# Check if the plugin registry needs to be reloaded # Check if the plugin registry needs to be reloaded
registry.check_reload() registry.check_reload()

View File

@ -58,10 +58,10 @@ class ScheduleMixin:
@classmethod @classmethod
def _activate_mixin(cls, registry, plugins, *args, **kwargs): def _activate_mixin(cls, registry, plugins, *args, **kwargs):
"""Activate schedules from plugins with the ScheduleMixin.""" """Activate schedules from plugins with the ScheduleMixin."""
logger.debug('Activating plugin tasks')
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
logger.debug('Activating plugin tasks')
# List of tasks we have activated # List of tasks we have activated
task_keys = [] task_keys = []

View File

@ -19,7 +19,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
def test_assert_error(barcode_data): def test_assert_error(barcode_data):
response = self.post( response = self.post(
reverse('api-barcode-link'), reverse('api-barcode-link'),
format='json',
data={'barcode': barcode_data, 'stockitem': 521}, data={'barcode': barcode_data, 'stockitem': 521},
expected_code=400, expected_code=400,
) )

View File

@ -198,7 +198,7 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
result1 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400) result1 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400)
assert result1.data['error'].startswith('No matching purchase order') self.assertTrue(result1.data['error'].startswith('No matching purchase order'))
self.purchase_order1.place_order() self.purchase_order1.place_order()
@ -211,8 +211,10 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
result4 = self.post( result4 = self.post(
url, data={'barcode': DIGIKEY_BARCODE[:-1]}, expected_code=400 url, data={'barcode': DIGIKEY_BARCODE[:-1]}, expected_code=400
) )
assert result4.data['error'].startswith( self.assertTrue(
'Failed to find pending line item for supplier part' result4.data['error'].startswith(
'Failed to find pending line item for supplier part'
)
) )
result5 = self.post( result5 = self.post(
@ -221,38 +223,42 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
expected_code=200, expected_code=200,
) )
stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk'])
assert stock_item.supplier_part.SKU == '296-LM358BIDDFRCT-ND' self.assertEqual(stock_item.supplier_part.SKU, '296-LM358BIDDFRCT-ND')
assert stock_item.quantity == 10 self.assertEqual(stock_item.quantity, 10)
assert stock_item.location is None self.assertEqual(stock_item.location, None)
def test_receive_custom_order_number(self): def test_receive_custom_order_number(self):
"""Test receiving an item from a barcode with a custom order number.""" """Test receiving an item from a barcode with a custom order number."""
url = reverse('api-barcode-po-receive') url = reverse('api-barcode-po-receive')
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}) result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
assert 'success' in result1.data self.assertIn('success', result1.data)
result2 = self.post( result2 = self.post(
reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE} reverse('api-barcode-scan'),
data={'barcode': MOUSER_BARCODE},
expected_code=200,
) )
stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
assert stock_item.supplier_part.SKU == '42' self.assertEqual(stock_item.supplier_part.SKU, '42')
assert stock_item.supplier_part.manufacturer_part.MPN == 'MC34063ADR' self.assertEqual(stock_item.supplier_part.manufacturer_part.MPN, 'MC34063ADR')
assert stock_item.quantity == 3 self.assertEqual(stock_item.quantity, 3)
assert stock_item.location is None self.assertEqual(stock_item.location, None)
def test_receive_one_stock_location(self): def test_receive_one_stock_location(self):
"""Test receiving an item when only one stock location exists.""" """Test receiving an item when only one stock location exists."""
stock_location = StockLocation.objects.create(name='Test Location') stock_location = StockLocation.objects.create(name='Test Location')
url = reverse('api-barcode-po-receive') url = reverse('api-barcode-po-receive')
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}) result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
assert 'success' in result1.data self.assertIn('success', result1.data)
result2 = self.post( result2 = self.post(
reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE} reverse('api-barcode-scan'),
data={'barcode': MOUSER_BARCODE},
expected_code=200,
) )
stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
assert stock_item.location == stock_location self.assertEqual(stock_item.location, stock_location)
def test_receive_default_line_item_location(self): def test_receive_default_line_item_location(self):
"""Test receiving an item into the default line_item location.""" """Test receiving an item into the default line_item location."""
@ -264,14 +270,16 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
line_item.save() line_item.save()
url = reverse('api-barcode-po-receive') url = reverse('api-barcode-po-receive')
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}) result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
assert 'success' in result1.data self.assertIn('success', result1.data)
result2 = self.post( result2 = self.post(
reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE} reverse('api-barcode-scan'),
data={'barcode': MOUSER_BARCODE},
expected_code=200,
) )
stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
assert stock_item.location == stock_location2 self.assertEqual(stock_item.location, stock_location2)
def test_receive_default_part_location(self): def test_receive_default_part_location(self):
"""Test receiving an item into the default part location.""" """Test receiving an item into the default part location."""
@ -283,14 +291,16 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
part.save() part.save()
url = reverse('api-barcode-po-receive') url = reverse('api-barcode-po-receive')
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}) result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
assert 'success' in result1.data self.assertIn('success', result1.data)
result2 = self.post( result2 = self.post(
reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE} reverse('api-barcode-scan'),
data={'barcode': MOUSER_BARCODE},
expected_code=200,
) )
stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
assert stock_item.location == stock_location2 self.assertEqual(stock_item.location, stock_location2)
def test_receive_specific_order_and_location(self): def test_receive_specific_order_and_location(self):
"""Test receiving an item from a specific order into a specific location.""" """Test receiving an item from a specific order into a specific location."""
@ -306,12 +316,15 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
'purchase_order': self.purchase_order2.pk, 'purchase_order': self.purchase_order2.pk,
'location': stock_location2.pk, 'location': stock_location2.pk,
}, },
expected_code=200,
) )
assert 'success' in result1.data self.assertIn('success', result1.data)
result2 = self.post(reverse('api-barcode-scan'), data={'barcode': barcode}) result2 = self.post(
reverse('api-barcode-scan'), data={'barcode': barcode}, expected_code=200
)
stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
assert stock_item.location == stock_location2 self.assertEqual(stock_item.location, stock_location2)
def test_receive_missing_quantity(self): def test_receive_missing_quantity(self):
"""Test receiving an with missing quantity information.""" """Test receiving an with missing quantity information."""
@ -319,8 +332,8 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
barcode = MOUSER_BARCODE.replace('\x1dQ3', '') barcode = MOUSER_BARCODE.replace('\x1dQ3', '')
response = self.post(url, data={'barcode': barcode}, expected_code=200) response = self.post(url, data={'barcode': barcode}, expected_code=200)
assert 'lineitem' in response.data self.assertIn('lineitem', response.data)
assert 'quantity' not in response.data['lineitem'] self.assertNotIn('quantity', response.data['lineitem'])
DIGIKEY_BARCODE = ( DIGIKEY_BARCODE = (

View File

@ -758,6 +758,16 @@ class PluginsRegistry:
# Some other exception, we want to know about it # Some other exception, we want to know about it
logger.exception('Failed to update plugin registry hash: %s', str(exc)) logger.exception('Failed to update plugin registry hash: %s', str(exc))
def plugin_settings_keys(self):
"""A list of keys which are used to store plugin settings."""
return [
'ENABLE_PLUGINS_URL',
'ENABLE_PLUGINS_NAVIGATION',
'ENABLE_PLUGINS_APP',
'ENABLE_PLUGINS_SCHEDULE',
'ENABLE_PLUGINS_EVENTS',
]
def calculate_plugin_hash(self): def calculate_plugin_hash(self):
"""Calculate a 'hash' value for the current registry. """Calculate a 'hash' value for the current registry.
@ -777,31 +787,24 @@ class PluginsRegistry:
data.update(str(plug.version).encode()) data.update(str(plug.version).encode())
data.update(str(plug.is_active()).encode()) data.update(str(plug.is_active()).encode())
# Also hash for all config settings which define plugin behavior for k in self.plugin_settings_keys():
keys = [
'ENABLE_PLUGINS_URL',
'ENABLE_PLUGINS_NAVIGATION',
'ENABLE_PLUGINS_APP',
'ENABLE_PLUGINS_SCHEDULE',
'ENABLE_PLUGINS_EVENTS',
]
for k in keys:
try: try:
data.update( val = InvenTreeSetting.get_setting(k, False, cache=False, create=False)
str( msg = f'{k}-{val}'
InvenTreeSetting.get_setting(
k, False, cache=False, create=False data.update(msg.encode())
)
).encode()
)
except Exception: except Exception:
pass pass
return str(data.hexdigest()) return str(data.hexdigest())
def check_reload(self): def check_reload(self):
"""Determine if the registry needs to be reloaded.""" """Determine if the registry needs to be reloaded.
- If a "request" object is available, then we can cache the result and attach it.
- The assumption is that plugins will not change during a single request.
"""
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
if settings.TESTING: if settings.TESTING:

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from common.notifications import storage from common.notifications import storage
from plugin import registry from plugin.registry import registry
register = template.Library() register = template.Library()

View File

@ -35,18 +35,25 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf', 'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
}, },
expected_code=400, expected_code=400,
max_query_time=20,
) )
# valid - Pypi # valid - Pypi
data = self.post( data = self.post(
url, {'confirm': True, 'packagename': self.PKG_NAME}, expected_code=201 url,
{'confirm': True, 'packagename': self.PKG_NAME},
expected_code=201,
max_query_time=20,
).data ).data
self.assertEqual(data['success'], 'Installed plugin successfully') self.assertEqual(data['success'], 'Installed plugin successfully')
# valid - github url # valid - github url
data = self.post( data = self.post(
url, {'confirm': True, 'url': self.PKG_URL}, expected_code=201 url,
{'confirm': True, 'url': self.PKG_URL},
expected_code=201,
max_query_time=20,
).data ).data
self.assertEqual(data['success'], 'Installed plugin successfully') self.assertEqual(data['success'], 'Installed plugin successfully')
@ -56,6 +63,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
url, url,
{'confirm': True, 'url': self.PKG_URL, 'packagename': self.PKG_NAME}, {'confirm': True, 'url': self.PKG_URL, 'packagename': self.PKG_NAME},
expected_code=201, expected_code=201,
max_query_time=20,
).data ).data
self.assertEqual(data['success'], 'Installed plugin successfully') self.assertEqual(data['success'], 'Installed plugin successfully')

View File

@ -9,15 +9,12 @@ PLUGIN_BASE = 'plugin' # Constant for links
def get_plugin_urls(): def get_plugin_urls():
"""Returns a urlpattern that can be integrated into the global urls.""" """Returns a urlpattern that can be integrated into the global urls."""
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from plugin import registry from plugin.registry import registry
urls = [] urls = []
# Only allow custom routing if the setting is enabled
if ( if (
InvenTreeSetting.get_setting( InvenTreeSetting.get_setting('ENABLE_PLUGINS_URL', False)
'ENABLE_PLUGINS_URL', False, create=False, cache=False
)
or settings.PLUGIN_TESTING_SETUP or settings.PLUGIN_TESTING_SETUP
): ):
for plugin in registry.plugins.values(): for plugin in registry.plugins.values():

View File

@ -6,6 +6,8 @@ from django.db import connection, migrations
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
import InvenTree.ready
def label_model_map(): def label_model_map():
"""Map legacy label template models to model_type values.""" """Map legacy label template models to model_type values."""
@ -43,7 +45,8 @@ def convert_legacy_labels(table_name, model_name, template_model):
cursor.execute(query) cursor.execute(query)
except Exception: except Exception:
# Table likely does not exist # Table likely does not exist
print(f"Legacy label table {table_name} not found - skipping migration") if not InvenTree.ready.isInTestMode():
print(f"Legacy label table {table_name} not found - skipping migration")
return 0 return 0
rows = cursor.fetchall() rows = cursor.fetchall()

View File

@ -446,6 +446,8 @@ class PrintTestMixins:
'items': [item.pk for item in qs], 'items': [item.pk for item in qs],
}, },
expected_code=201, expected_code=201,
max_query_time=15,
max_query_count=1000, # TODO: Should look into this
) )

View File

@ -2160,7 +2160,7 @@ class StockItem(
"""Return a list of test-result objects for this StockItem.""" """Return a list of test-result objects for this StockItem."""
return list(self.testResultMap(**kwargs).values()) return list(self.testResultMap(**kwargs).values())
def requiredTestStatus(self): def requiredTestStatus(self, required_tests=None):
"""Return the status of the tests required for this StockItem. """Return the status of the tests required for this StockItem.
Return: Return:
@ -2170,15 +2170,17 @@ class StockItem(
- failed: Number of tests that have failed - failed: Number of tests that have failed
""" """
# All the tests required by the part object # All the tests required by the part object
required = self.part.getRequiredTests()
if required_tests is None:
required_tests = self.part.getRequiredTests()
results = self.testResultMap() results = self.testResultMap()
total = len(required) total = len(required_tests)
passed = 0 passed = 0
failed = 0 failed = 0
for test in required: for test in required_tests:
key = InvenTree.helpers.generateTestKey(test.test_name) key = InvenTree.helpers.generateTestKey(test.test_name)
if key in results: if key in results:
@ -2200,9 +2202,9 @@ class StockItem(
"""Return True if there are any 'required tests' associated with this StockItem.""" """Return True if there are any 'required tests' associated with this StockItem."""
return self.required_test_count > 0 return self.required_test_count > 0
def passedAllRequiredTests(self): def passedAllRequiredTests(self, required_tests=None):
"""Returns True if this StockItem has passed all required tests.""" """Returns True if this StockItem has passed all required tests."""
status = self.requiredTestStatus() status = self.requiredTestStatus(required_tests=required_tests)
return status['passed'] >= status['total'] return status['passed'] >= status['total']

View File

@ -1304,10 +1304,13 @@ class StockItemTest(StockAPITestCase):
self.assertIn('This field is required', str(response.data['location'])) self.assertIn('This field is required', str(response.data['location']))
# TODO: Return to this and work out why it is taking so long
# Ref: https://github.com/inventree/InvenTree/pull/7157
response = self.post( response = self.post(
url, url,
{'location': '1', 'notes': 'Returned from this customer for testing'}, {'location': '1', 'notes': 'Returned from this customer for testing'},
expected_code=201, expected_code=201,
max_query_time=5.0,
) )
item.refresh_from_db() item.refresh_from_db()
@ -1417,7 +1420,7 @@ class StocktakeTest(StockAPITestCase):
data = {} data = {}
# POST with a valid action # POST with a valid action
response = self.post(url, data) response = self.post(url, data, expected_code=400)
self.assertIn('This field is required', str(response.data['items'])) self.assertIn('This field is required', str(response.data['items']))
@ -1452,7 +1455,7 @@ class StocktakeTest(StockAPITestCase):
# POST with an invalid quantity value # POST with an invalid quantity value
data['items'] = [{'pk': 1234, 'quantity': '10x0d'}] data['items'] = [{'pk': 1234, 'quantity': '10x0d'}]
response = self.post(url, data) response = self.post(url, data, expected_code=400)
self.assertContains( self.assertContains(
response, response,
'A valid number is required', 'A valid number is required',
@ -1461,7 +1464,8 @@ class StocktakeTest(StockAPITestCase):
data['items'] = [{'pk': 1234, 'quantity': '-1.234'}] data['items'] = [{'pk': 1234, 'quantity': '-1.234'}]
response = self.post(url, data) response = self.post(url, data, expected_code=400)
self.assertContains( self.assertContains(
response, response,
'Ensure this value is greater than or equal to 0', 'Ensure this value is greater than or equal to 0',

View File

@ -613,9 +613,9 @@ def update_group_roles(group, debug=False):
content_type=content_type, codename=perm content_type=content_type, codename=perm
) )
except ContentType.DoesNotExist: # pragma: no cover except ContentType.DoesNotExist: # pragma: no cover
logger.warning( # logger.warning(
"Error: Could not find permission matching '%s'", permission_string # "Error: Could not find permission matching '%s'", permission_string
) # )
permission = None permission = None
return permission return permission