mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
c196511327
commit
fb193cae3d
@ -273,13 +273,14 @@ if DEBUG and get_boolean_setting(
|
||||
'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False
|
||||
):
|
||||
MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware')
|
||||
logger.debug('Running with debug_querycount middleware enabled')
|
||||
|
||||
QUERYCOUNT = {
|
||||
'THRESHOLDS': {
|
||||
'MEDIUM': 50,
|
||||
'HIGH': 200,
|
||||
'MIN_TIME_TO_LOG': 0,
|
||||
'MIN_QUERY_COUNT_TO_LOG': 0,
|
||||
'MIN_TIME_TO_LOG': 0.1,
|
||||
'MIN_QUERY_COUNT_TO_LOG': 25,
|
||||
},
|
||||
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
|
||||
'IGNORE_SQL_PATTERNS': [],
|
||||
|
@ -4,6 +4,7 @@ import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@ -228,10 +229,19 @@ class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
|
||||
class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
"""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
|
||||
def assertNumQueriesLessThan(
|
||||
self, value, using='default', verbose=False, debug=False
|
||||
):
|
||||
def assertNumQueriesLessThan(self, value, using='default', verbose=None, url=None):
|
||||
"""Context manager to check that the number of queries is less than a certain value.
|
||||
|
||||
Example:
|
||||
@ -242,6 +252,13 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
with CaptureQueriesContext(connections[using]) as context:
|
||||
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:
|
||||
msg = '\r\n%s' % json.dumps(
|
||||
context.captured_queries, indent=4
|
||||
@ -249,34 +266,29 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||
else:
|
||||
msg = None
|
||||
|
||||
n = len(context.captured_queries)
|
||||
|
||||
if debug:
|
||||
print(
|
||||
f'Expected less than {value} queries, got {n} queries'
|
||||
) # pragma: no cover
|
||||
if url and n > self.WARNING_QUERY_THRESHOLD:
|
||||
print(f'Warning: {n} queries executed at {url}')
|
||||
|
||||
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."""
|
||||
# No expected code, return
|
||||
if expected_code is None:
|
||||
return
|
||||
# Check that the response returned the expected status code
|
||||
|
||||
if expected_code != response.status_code: # pragma: no cover
|
||||
print(
|
||||
f"Unexpected {method} response at '{url}': status_code = {response.status_code}"
|
||||
)
|
||||
if expected_code is not None:
|
||||
if expected_code != response.status_code: # pragma: no cover
|
||||
print(
|
||||
f"Unexpected response at '{url}': status_code = {response.status_code} (expected {expected_code})"
|
||||
)
|
||||
|
||||
if hasattr(response, 'data'):
|
||||
print('data:', response.data)
|
||||
if hasattr(response, 'body'):
|
||||
print('body:', response.body)
|
||||
if hasattr(response, 'content'):
|
||||
print('content:', response.content)
|
||||
if hasattr(response, 'data'):
|
||||
print('data:', response.data)
|
||||
if hasattr(response, 'body'):
|
||||
print('body:', response.body)
|
||||
if hasattr(response, 'content'):
|
||||
print('content:', response.content)
|
||||
|
||||
self.assertEqual(expected_code, response.status_code)
|
||||
self.assertEqual(expected_code, response.status_code)
|
||||
|
||||
def getActions(self, url):
|
||||
"""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', {})
|
||||
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."""
|
||||
# Set default - see B006
|
||||
if data is None:
|
||||
data = {}
|
||||
kwargs['data'] = 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)
|
||||
|
||||
return response
|
||||
|
||||
def post(self, url, data=None, expected_code=None, format='json', **kwargs):
|
||||
def post(self, url, data=None, expected_code=201, **kwargs):
|
||||
"""Issue a POST request."""
|
||||
# Set default value - see B006
|
||||
if data is None:
|
||||
data = {}
|
||||
# Default query limit is higher for POST requests, due to extra event processing
|
||||
kwargs['max_query_count'] = kwargs.get(
|
||||
'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=None, format='json', **kwargs):
|
||||
def delete(self, url, data=None, expected_code=204, **kwargs):
|
||||
"""Issue a DELETE request."""
|
||||
if data is None:
|
||||
data = {}
|
||||
kwargs['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)
|
||||
|
||||
return response
|
||||
|
||||
def patch(self, url, data, expected_code=None, format='json', **kwargs):
|
||||
def patch(self, url, data, expected_code=200, **kwargs):
|
||||
"""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=None, format='json', **kwargs):
|
||||
def put(self, url, data, expected_code=200, **kwargs):
|
||||
"""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 response
|
||||
return self.query(url, self.client.put, expected_code=expected_code, **kwargs)
|
||||
|
||||
def options(self, url, expected_code=None, **kwargs):
|
||||
"""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 response
|
||||
return self.query(
|
||||
url, self.client.options, expected_code=expected_code, **kwargs
|
||||
)
|
||||
|
||||
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."""
|
||||
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
|
||||
if not isinstance(response, StreamingHttpResponse):
|
||||
|
@ -567,7 +567,7 @@ class Build(
|
||||
self.allocated_stock.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def complete_build(self, user):
|
||||
def complete_build(self, user, trim_allocated_stock=False):
|
||||
"""Mark this build as complete."""
|
||||
|
||||
import build.tasks
|
||||
@ -575,6 +575,9 @@ class Build(
|
||||
if self.incomplete_count > 0:
|
||||
return
|
||||
|
||||
if trim_allocated_stock:
|
||||
self.trim_allocated_stock()
|
||||
|
||||
self.completion_date = InvenTree.helpers.current_date()
|
||||
self.completed_by = user
|
||||
self.status = BuildStatus.COMPLETE.value
|
||||
@ -858,6 +861,10 @@ class Build(
|
||||
def trim_allocated_stock(self):
|
||||
"""Called after save to reduce allocated stock if the build order is now overallocated."""
|
||||
# Only need to worry about untracked stock here
|
||||
|
||||
items_to_save = []
|
||||
items_to_delete = []
|
||||
|
||||
for build_line in self.untracked_line_items:
|
||||
|
||||
reduce_by = build_line.allocated_quantity() - build_line.quantity
|
||||
@ -875,13 +882,19 @@ class Build(
|
||||
# Easy case - this item can just be reduced.
|
||||
if item.quantity > reduce_by:
|
||||
item.quantity -= reduce_by
|
||||
item.save()
|
||||
items_to_save.append(item)
|
||||
break
|
||||
|
||||
# Harder case, this item needs to be deleted, and any remainder
|
||||
# taken from the next items in the list.
|
||||
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
|
||||
def allocated_stock(self):
|
||||
@ -978,7 +991,10 @@ class Build(
|
||||
# List the allocated BuildItem objects for the given output
|
||||
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
|
||||
raise ValidationError(
|
||||
_(f"Build output {serial} has not passed all required tests"))
|
||||
|
@ -568,10 +568,13 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
|
||||
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"
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
|
||||
output = item['output']
|
||||
|
||||
build.complete_build_output(
|
||||
@ -580,6 +583,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
location=location,
|
||||
status=status,
|
||||
notes=notes,
|
||||
required_tests=required_tests,
|
||||
prevent_on_incomplete=prevent_on_incomplete,
|
||||
)
|
||||
|
||||
|
||||
@ -734,10 +739,11 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
build = self.context['build']
|
||||
|
||||
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):
|
||||
|
@ -223,6 +223,7 @@ class BuildTest(BuildAPITest):
|
||||
"status": 50, # Item requires attention
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=450, # TODO: Try to optimize this
|
||||
)
|
||||
|
||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||
@ -992,6 +993,7 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
'accept_overallocated': 'accept',
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=550, # TODO: Come back and refactor this
|
||||
)
|
||||
|
||||
self.build.refresh_from_db()
|
||||
@ -1012,6 +1014,7 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
'accept_overallocated': 'trim',
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=550, # TODO: Come back and refactor this
|
||||
)
|
||||
|
||||
self.build.refresh_from_db()
|
||||
|
@ -749,6 +749,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
attempts=attempts - 1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
except (OperationalError, ProgrammingError):
|
||||
logger.warning("Database is locked, cannot set setting '%s'", key)
|
||||
# Likely the DB is locked - not much we can do here
|
||||
|
@ -1093,7 +1093,7 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
||||
|
||||
# Updating via the external exchange may not work every time
|
||||
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
|
||||
if Rate.objects.all().exists():
|
||||
|
@ -1108,7 +1108,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
|
||||
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
|
||||
self.assertEqual(n + 11, StockItem.objects.count())
|
||||
|
@ -371,8 +371,6 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
params['delete_child_categories'] = '1'
|
||||
response = self.delete(url, params, expected_code=204)
|
||||
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
if delete_parts:
|
||||
if i == Target.delete_subcategories_delete_parts:
|
||||
# Check if all parts deleted
|
||||
@ -685,7 +683,6 @@ class PartAPITest(PartAPITestBase):
|
||||
# Request *all* part categories
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 8)
|
||||
|
||||
# Request top-level part categories only
|
||||
@ -709,7 +706,6 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
url = reverse('api-part-category-list')
|
||||
response = self.post(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
parent = response.data['pk']
|
||||
|
||||
@ -717,7 +713,6 @@ class PartAPITest(PartAPITestBase):
|
||||
for animal in ['cat', 'dog', 'zebra']:
|
||||
data = {'name': animal, 'description': 'A sort of animal', 'parent': parent}
|
||||
response = self.post(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['parent'], parent)
|
||||
self.assertEqual(response.data['name'], animal)
|
||||
self.assertEqual(response.data['pathstring'], 'Animals/' + animal)
|
||||
@ -741,7 +736,6 @@ class PartAPITest(PartAPITestBase):
|
||||
data['parent'] = None
|
||||
data['description'] = 'Changing the description'
|
||||
response = self.patch(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['description'], 'Changing the description')
|
||||
self.assertIsNone(response.data['parent'])
|
||||
|
||||
@ -750,13 +744,11 @@ class PartAPITest(PartAPITestBase):
|
||||
url = reverse('api-part-list')
|
||||
data = {'cascade': True}
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), Part.objects.count())
|
||||
|
||||
# Test filtering parts by category
|
||||
data = {'category': 2}
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# There should only be 2 objects in category C
|
||||
self.assertEqual(len(response.data), 2)
|
||||
@ -897,7 +889,6 @@ class PartAPITest(PartAPITestBase):
|
||||
response = self.get(url, data)
|
||||
|
||||
# Now there should be 5 total parts
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_test_templates(self):
|
||||
@ -906,8 +897,6 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
# List ALL items
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 9)
|
||||
|
||||
# Request for a particular part
|
||||
@ -921,10 +910,9 @@ class PartAPITest(PartAPITestBase):
|
||||
response = self.post(
|
||||
url,
|
||||
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)
|
||||
response = self.post(
|
||||
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)
|
||||
response = self.post(
|
||||
url,
|
||||
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)
|
||||
response = self.post(url, data={'part': 1, 'test_name': 'A simple test'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
response = self.post(
|
||||
url, data={'part': 1, 'test_name': 'A simple test'}, expected_code=400
|
||||
)
|
||||
|
||||
def test_get_thumbs(self):
|
||||
"""Return list of part thumbnails."""
|
||||
@ -957,8 +942,6 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_paginate(self):
|
||||
"""Test pagination of the Part list API."""
|
||||
for n in [1, 5, 10]:
|
||||
@ -1450,8 +1433,6 @@ class PartDetailTests(PartAPITestBase):
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
pk = response.data['pk']
|
||||
|
||||
# Check that a new part has been added
|
||||
@ -1470,7 +1451,6 @@ class PartDetailTests(PartAPITestBase):
|
||||
|
||||
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['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
|
||||
response = self.patch(url, {'name': 'a new better name'})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Try to remove a tag
|
||||
response = self.patch(url, {'tags': ['tag1']})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['tags'], ['tag1'])
|
||||
|
||||
# Try to remove the part
|
||||
response = self.delete(url)
|
||||
|
||||
# As the part is 'active' we cannot delete it
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = self.delete(url, expected_code=400)
|
||||
|
||||
# So, let's make it not active
|
||||
response = self.patch(url, {'active': False}, expected_code=200)
|
||||
|
||||
response = self.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
# Part count should have reduced
|
||||
self.assertEqual(Part.objects.count(), n)
|
||||
@ -1522,8 +1495,6 @@ class PartDetailTests(PartAPITestBase):
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
n = Part.objects.count()
|
||||
|
||||
# Check that we cannot create a duplicate in a different category
|
||||
@ -1536,10 +1507,9 @@ class PartDetailTests(PartAPITestBase):
|
||||
'category': 2,
|
||||
'revision': 'A',
|
||||
},
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Check that only 1 matching part exists
|
||||
parts = Part.objects.filter(
|
||||
name='part', description='description', IPN='IPN-123'
|
||||
@ -1560,9 +1530,9 @@ class PartDetailTests(PartAPITestBase):
|
||||
'category': 2,
|
||||
'revision': 'B',
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Part.objects.count(), n + 1)
|
||||
|
||||
# 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})
|
||||
|
||||
# Attempt to alter the revision code
|
||||
response = self.patch(url, {'revision': 'A'})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = self.patch(url, {'revision': 'A'}, expected_code=400)
|
||||
|
||||
# But we *can* change it to a unique revision code
|
||||
response = self.patch(url, {'revision': 'C'})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.patch(url, {'revision': 'C'}, expected_code=200)
|
||||
|
||||
def test_image_upload(self):
|
||||
"""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:
|
||||
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))
|
||||
|
||||
# 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:
|
||||
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
|
||||
p = Part.objects.get(pk=pk)
|
||||
self.assertIsNotNone(p.image)
|
||||
@ -1644,10 +1607,11 @@ class PartDetailTests(PartAPITestBase):
|
||||
|
||||
with open(fn, 'rb') as img_file:
|
||||
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']
|
||||
self.assertTrue(image_name.startswith('/media/part_images/part_image'))
|
||||
|
||||
@ -1690,10 +1654,11 @@ class PartDetailTests(PartAPITestBase):
|
||||
# Upload the image to a part
|
||||
with open(fn, 'rb') as img_file:
|
||||
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']
|
||||
self.assertTrue(image_name.startswith('/media/part_images/part_image'))
|
||||
|
||||
@ -1949,8 +1914,6 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
for part in response.data:
|
||||
if part['pk'] == self.part.pk:
|
||||
return part
|
||||
@ -2677,8 +2640,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
|
||||
|
||||
InvenTreeSetting.set_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', True)
|
||||
|
||||
response = self.delete(reverse('api-part-detail', kwargs={'pk': 1}))
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.delete(reverse('api-part-detail', kwargs={'pk': 1}))
|
||||
|
||||
with self.assertRaises(Part.DoesNotExist):
|
||||
p.refresh_from_db()
|
||||
|
@ -27,25 +27,19 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
def postBarcode(self, url, barcode, expected_code=None):
|
||||
"""Post barcode and return results."""
|
||||
return self.post(
|
||||
url,
|
||||
format='json',
|
||||
data={'barcode': str(barcode)},
|
||||
expected_code=expected_code,
|
||||
url, data={'barcode': str(barcode)}, expected_code=expected_code
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test that invalid requests fail."""
|
||||
# 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
|
||||
self.post(self.assign_url, format='json', data={}, expected_code=400)
|
||||
self.post(
|
||||
self.assign_url, format='json', data={'barcode': '123'}, expected_code=400
|
||||
)
|
||||
self.post(self.assign_url, data={}, expected_code=400)
|
||||
self.post(self.assign_url, data={'barcode': '123'}, expected_code=400)
|
||||
self.post(
|
||||
self.assign_url,
|
||||
format='json',
|
||||
data={'barcode': '123', 'stockitem': '123'},
|
||||
expected_code=400,
|
||||
)
|
||||
@ -163,7 +157,6 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
|
||||
response = self.post(
|
||||
self.assign_url,
|
||||
format='json',
|
||||
data={'barcode': barcode_data, 'stockitem': item.pk},
|
||||
expected_code=200,
|
||||
)
|
||||
@ -183,7 +176,6 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
# Ensure that the same barcode hash cannot be assigned to a different stock item!
|
||||
response = self.post(
|
||||
self.assign_url,
|
||||
format='json',
|
||||
data={'barcode': barcode_data, 'stockitem': 521},
|
||||
expected_code=400,
|
||||
)
|
||||
|
@ -23,10 +23,6 @@ def trigger_event(event, *args, **kwargs):
|
||||
"""
|
||||
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):
|
||||
# Do nothing if plugin events are not enabled
|
||||
return
|
||||
@ -59,7 +55,9 @@ def register_event(event, *args, **kwargs):
|
||||
logger.debug("Registering triggered event: '%s'", event)
|
||||
|
||||
# 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
|
||||
registry.check_reload()
|
||||
|
||||
|
@ -58,10 +58,10 @@ class ScheduleMixin:
|
||||
@classmethod
|
||||
def _activate_mixin(cls, registry, plugins, *args, **kwargs):
|
||||
"""Activate schedules from plugins with the ScheduleMixin."""
|
||||
logger.debug('Activating plugin tasks')
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
logger.debug('Activating plugin tasks')
|
||||
|
||||
# List of tasks we have activated
|
||||
task_keys = []
|
||||
|
||||
|
@ -19,7 +19,6 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
def test_assert_error(barcode_data):
|
||||
response = self.post(
|
||||
reverse('api-barcode-link'),
|
||||
format='json',
|
||||
data={'barcode': barcode_data, 'stockitem': 521},
|
||||
expected_code=400,
|
||||
)
|
||||
|
@ -198,7 +198,7 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
|
||||
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()
|
||||
|
||||
@ -211,8 +211,10 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
result4 = self.post(
|
||||
url, data={'barcode': DIGIKEY_BARCODE[:-1]}, expected_code=400
|
||||
)
|
||||
assert result4.data['error'].startswith(
|
||||
'Failed to find pending line item for supplier part'
|
||||
self.assertTrue(
|
||||
result4.data['error'].startswith(
|
||||
'Failed to find pending line item for supplier part'
|
||||
)
|
||||
)
|
||||
|
||||
result5 = self.post(
|
||||
@ -221,38 +223,42 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
expected_code=200,
|
||||
)
|
||||
stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk'])
|
||||
assert stock_item.supplier_part.SKU == '296-LM358BIDDFRCT-ND'
|
||||
assert stock_item.quantity == 10
|
||||
assert stock_item.location is None
|
||||
self.assertEqual(stock_item.supplier_part.SKU, '296-LM358BIDDFRCT-ND')
|
||||
self.assertEqual(stock_item.quantity, 10)
|
||||
self.assertEqual(stock_item.location, None)
|
||||
|
||||
def test_receive_custom_order_number(self):
|
||||
"""Test receiving an item from a barcode with a custom order number."""
|
||||
url = reverse('api-barcode-po-receive')
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
|
||||
assert 'success' in result1.data
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
|
||||
self.assertIn('success', result1.data)
|
||||
|
||||
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'])
|
||||
assert stock_item.supplier_part.SKU == '42'
|
||||
assert stock_item.supplier_part.manufacturer_part.MPN == 'MC34063ADR'
|
||||
assert stock_item.quantity == 3
|
||||
assert stock_item.location is None
|
||||
self.assertEqual(stock_item.supplier_part.SKU, '42')
|
||||
self.assertEqual(stock_item.supplier_part.manufacturer_part.MPN, 'MC34063ADR')
|
||||
self.assertEqual(stock_item.quantity, 3)
|
||||
self.assertEqual(stock_item.location, None)
|
||||
|
||||
def test_receive_one_stock_location(self):
|
||||
"""Test receiving an item when only one stock location exists."""
|
||||
stock_location = StockLocation.objects.create(name='Test Location')
|
||||
|
||||
url = reverse('api-barcode-po-receive')
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
|
||||
assert 'success' in result1.data
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
|
||||
self.assertIn('success', result1.data)
|
||||
|
||||
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'])
|
||||
assert stock_item.location == stock_location
|
||||
self.assertEqual(stock_item.location, stock_location)
|
||||
|
||||
def test_receive_default_line_item_location(self):
|
||||
"""Test receiving an item into the default line_item location."""
|
||||
@ -264,14 +270,16 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
line_item.save()
|
||||
|
||||
url = reverse('api-barcode-po-receive')
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
|
||||
assert 'success' in result1.data
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
|
||||
self.assertIn('success', result1.data)
|
||||
|
||||
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'])
|
||||
assert stock_item.location == stock_location2
|
||||
self.assertEqual(stock_item.location, stock_location2)
|
||||
|
||||
def test_receive_default_part_location(self):
|
||||
"""Test receiving an item into the default part location."""
|
||||
@ -283,14 +291,16 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
part.save()
|
||||
|
||||
url = reverse('api-barcode-po-receive')
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
|
||||
assert 'success' in result1.data
|
||||
result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
|
||||
self.assertIn('success', result1.data)
|
||||
|
||||
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'])
|
||||
assert stock_item.location == stock_location2
|
||||
self.assertEqual(stock_item.location, stock_location2)
|
||||
|
||||
def test_receive_specific_order_and_location(self):
|
||||
"""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,
|
||||
'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'])
|
||||
assert stock_item.location == stock_location2
|
||||
self.assertEqual(stock_item.location, stock_location2)
|
||||
|
||||
def test_receive_missing_quantity(self):
|
||||
"""Test receiving an with missing quantity information."""
|
||||
@ -319,8 +332,8 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
barcode = MOUSER_BARCODE.replace('\x1dQ3', '')
|
||||
response = self.post(url, data={'barcode': barcode}, expected_code=200)
|
||||
|
||||
assert 'lineitem' in response.data
|
||||
assert 'quantity' not in response.data['lineitem']
|
||||
self.assertIn('lineitem', response.data)
|
||||
self.assertNotIn('quantity', response.data['lineitem'])
|
||||
|
||||
|
||||
DIGIKEY_BARCODE = (
|
||||
|
@ -758,6 +758,16 @@ class PluginsRegistry:
|
||||
# Some other exception, we want to know about it
|
||||
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):
|
||||
"""Calculate a 'hash' value for the current registry.
|
||||
|
||||
@ -777,31 +787,24 @@ class PluginsRegistry:
|
||||
data.update(str(plug.version).encode())
|
||||
data.update(str(plug.is_active()).encode())
|
||||
|
||||
# Also hash for all config settings which define plugin behavior
|
||||
keys = [
|
||||
'ENABLE_PLUGINS_URL',
|
||||
'ENABLE_PLUGINS_NAVIGATION',
|
||||
'ENABLE_PLUGINS_APP',
|
||||
'ENABLE_PLUGINS_SCHEDULE',
|
||||
'ENABLE_PLUGINS_EVENTS',
|
||||
]
|
||||
|
||||
for k in keys:
|
||||
for k in self.plugin_settings_keys():
|
||||
try:
|
||||
data.update(
|
||||
str(
|
||||
InvenTreeSetting.get_setting(
|
||||
k, False, cache=False, create=False
|
||||
)
|
||||
).encode()
|
||||
)
|
||||
val = InvenTreeSetting.get_setting(k, False, cache=False, create=False)
|
||||
msg = f'{k}-{val}'
|
||||
|
||||
data.update(msg.encode())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return str(data.hexdigest())
|
||||
|
||||
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
|
||||
|
||||
if settings.TESTING:
|
||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from common.notifications import storage
|
||||
from plugin import registry
|
||||
from plugin.registry import registry
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -35,18 +35,25 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
|
||||
},
|
||||
expected_code=400,
|
||||
max_query_time=20,
|
||||
)
|
||||
|
||||
# valid - Pypi
|
||||
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
|
||||
|
||||
self.assertEqual(data['success'], 'Installed plugin successfully')
|
||||
|
||||
# valid - github url
|
||||
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
|
||||
|
||||
self.assertEqual(data['success'], 'Installed plugin successfully')
|
||||
@ -56,6 +63,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
url,
|
||||
{'confirm': True, 'url': self.PKG_URL, 'packagename': self.PKG_NAME},
|
||||
expected_code=201,
|
||||
max_query_time=20,
|
||||
).data
|
||||
self.assertEqual(data['success'], 'Installed plugin successfully')
|
||||
|
||||
|
@ -9,15 +9,12 @@ PLUGIN_BASE = 'plugin' # Constant for links
|
||||
def get_plugin_urls():
|
||||
"""Returns a urlpattern that can be integrated into the global urls."""
|
||||
from common.models import InvenTreeSetting
|
||||
from plugin import registry
|
||||
from plugin.registry import registry
|
||||
|
||||
urls = []
|
||||
|
||||
# Only allow custom routing if the setting is enabled
|
||||
if (
|
||||
InvenTreeSetting.get_setting(
|
||||
'ENABLE_PLUGINS_URL', False, create=False, cache=False
|
||||
)
|
||||
InvenTreeSetting.get_setting('ENABLE_PLUGINS_URL', False)
|
||||
or settings.PLUGIN_TESTING_SETUP
|
||||
):
|
||||
for plugin in registry.plugins.values():
|
||||
|
@ -6,6 +6,8 @@ from django.db import connection, migrations
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import InvenTree.ready
|
||||
|
||||
|
||||
def label_model_map():
|
||||
"""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)
|
||||
except Exception:
|
||||
# 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
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
@ -446,6 +446,8 @@ class PrintTestMixins:
|
||||
'items': [item.pk for item in qs],
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_time=15,
|
||||
max_query_count=1000, # TODO: Should look into this
|
||||
)
|
||||
|
||||
|
||||
|
@ -2160,7 +2160,7 @@ class StockItem(
|
||||
"""Return a list of test-result objects for this StockItem."""
|
||||
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:
|
||||
@ -2170,15 +2170,17 @@ class StockItem(
|
||||
- failed: Number of tests that have failed
|
||||
"""
|
||||
# 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()
|
||||
|
||||
total = len(required)
|
||||
total = len(required_tests)
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in required:
|
||||
for test in required_tests:
|
||||
key = InvenTree.helpers.generateTestKey(test.test_name)
|
||||
|
||||
if key in results:
|
||||
@ -2200,9 +2202,9 @@ class StockItem(
|
||||
"""Return True if there are any 'required tests' associated with this StockItem."""
|
||||
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."""
|
||||
status = self.requiredTestStatus()
|
||||
status = self.requiredTestStatus(required_tests=required_tests)
|
||||
|
||||
return status['passed'] >= status['total']
|
||||
|
||||
|
@ -1304,10 +1304,13 @@ class StockItemTest(StockAPITestCase):
|
||||
|
||||
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(
|
||||
url,
|
||||
{'location': '1', 'notes': 'Returned from this customer for testing'},
|
||||
expected_code=201,
|
||||
max_query_time=5.0,
|
||||
)
|
||||
|
||||
item.refresh_from_db()
|
||||
@ -1417,7 +1420,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
data = {}
|
||||
|
||||
# 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']))
|
||||
|
||||
@ -1452,7 +1455,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
# POST with an invalid quantity value
|
||||
data['items'] = [{'pk': 1234, 'quantity': '10x0d'}]
|
||||
|
||||
response = self.post(url, data)
|
||||
response = self.post(url, data, expected_code=400)
|
||||
self.assertContains(
|
||||
response,
|
||||
'A valid number is required',
|
||||
@ -1461,7 +1464,8 @@ class StocktakeTest(StockAPITestCase):
|
||||
|
||||
data['items'] = [{'pk': 1234, 'quantity': '-1.234'}]
|
||||
|
||||
response = self.post(url, data)
|
||||
response = self.post(url, data, expected_code=400)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
'Ensure this value is greater than or equal to 0',
|
||||
|
@ -613,9 +613,9 @@ def update_group_roles(group, debug=False):
|
||||
content_type=content_type, codename=perm
|
||||
)
|
||||
except ContentType.DoesNotExist: # pragma: no cover
|
||||
logger.warning(
|
||||
"Error: Could not find permission matching '%s'", permission_string
|
||||
)
|
||||
# logger.warning(
|
||||
# "Error: Could not find permission matching '%s'", permission_string
|
||||
# )
|
||||
permission = None
|
||||
|
||||
return permission
|
||||
|
Loading…
Reference in New Issue
Block a user