From ffda7002443d4c394bab37846703184584986e7d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 4 Oct 2023 17:28:20 +1100 Subject: [PATCH] Improve unit conversion (#5658) - In previous implementation, could not have a value "10k" for a unit of "m" - Now, correct result is obtained (10k [m] = 10[km]) - Still passes all previous unit tests - Simpler code, too --- InvenTree/InvenTree/conversion.py | 69 ++++++++++++------------ InvenTree/InvenTree/tests.py | 52 ++++++++++-------- InvenTree/plugin/registry.py | 4 +- InvenTree/templates/js/translated/api.js | 4 ++ 4 files changed, 71 insertions(+), 58 deletions(-) diff --git a/InvenTree/InvenTree/conversion.py b/InvenTree/InvenTree/conversion.py index 0b66e2df9e..c69bd73c0c 100644 --- a/InvenTree/InvenTree/conversion.py +++ b/InvenTree/InvenTree/conversion.py @@ -85,65 +85,66 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True): The converted quantity, in the specified units """ + original = str(value).strip() + # Ensure that the value is a string - value = str(value).strip() + value = str(value).strip() if value else '' + unit = str(unit).strip() if unit else '' # Error on blank values if not value: raise ValidationError(_('No value provided')) + # Create a "backup" value which be tried if the first value fails + # e.g. value = "10k" and unit = "ohm" -> "10kohm" + # e.g. value = "10m" and unit = "F" -> "10mF" + if unit: + backup_value = value + unit + else: + backup_value = None + ureg = get_unit_registry() - error = '' try: - # Convert to a quantity - val = ureg.Quantity(value) + value = ureg.Quantity(value) if unit: - - if is_dimensionless(val): - # If the provided value is dimensionless, assume that the unit is correct - val = ureg.Quantity(value, unit) + if is_dimensionless(value): + magnitude = value.to_base_units().magnitude + value = ureg.Quantity(magnitude, unit) else: - # Convert to the provided unit (may raise an exception) - val = val.to(ureg.Unit(unit)) + value = value.to(unit) - # At this point we *should* have a valid pint value - # To double check, look at the maginitude - float(ureg.Quantity(val.magnitude).magnitude) - except (TypeError, ValueError, AttributeError): - error = _('Provided value is not a valid number') - except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError): - error = _('Provided value has an invalid unit') + except Exception: + if backup_value: + try: + value = ureg.Quantity(backup_value) + except Exception: + value = None + else: + value = None + + if value is None: if unit: - error += f' ({unit})' - - except pint.errors.DimensionalityError: - error = _('Provided value could not be converted to the specified unit') - if unit: - error += f' ({unit})' - - except Exception as e: - error = _('Error') + ': ' + str(e) - - if error: - raise ValidationError(error) + raise ValidationError(_(f'Could not convert {original} to {unit}')) + else: + raise ValidationError(_("Invalid quantity supplied")) # Calculate the "magnitude" of the value, as a float - # If the value is specified strangely (e.g. as a fraction or a dozen), this can cause isuses + # If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues # So, we ensure that it is converted to a floating point value # If we wish to return a "raw" value, some trickery is required if unit: - magnitude = ureg.Quantity(val.to(ureg.Unit(unit))).magnitude + magnitude = ureg.Quantity(value.to(ureg.Unit(unit))).magnitude else: - magnitude = ureg.Quantity(val.to_base_units()).magnitude + magnitude = ureg.Quantity(value.to_base_units()).magnitude magnitude = float(ureg.Quantity(magnitude).to_base_units().magnitude) if strip_units: return magnitude - elif unit or val.units: - return ureg.Quantity(magnitude, unit or val.units) + elif unit or value.units: + return ureg.Quantity(magnitude, unit or value.units) else: return ureg.Quantity(magnitude) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 8fec1fff28..cbb8bf58d1 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -42,6 +42,22 @@ from .validators import validate_overage class ConversionTest(TestCase): """Tests for conversion of physical units""" + def test_prefixes(self): + """Test inputs where prefixes are used""" + + tests = { + "3": 3, + "3m": 3, + "3mm": 0.003, + "3k": 3000, + "3u": 0.000003, + "3 inch": 0.0762, + } + + for val, expected in tests.items(): + q = InvenTree.conversion.convert_physical_value(val, 'm') + self.assertAlmostEqual(q, expected, 3) + def test_base_units(self): """Test conversion to specified base units""" tests = { @@ -56,14 +72,12 @@ class ConversionTest(TestCase): for val, expected in tests.items(): q = InvenTree.conversion.convert_physical_value(val, 'W') - - self.assertAlmostEqual(q, expected, 0.01) - + self.assertAlmostEqual(q, expected, places=2) q = InvenTree.conversion.convert_physical_value(val, 'W', strip_units=False) - self.assertAlmostEqual(float(q.magnitude), expected, 0.01) + self.assertAlmostEqual(float(q.magnitude), expected, places=2) def test_dimensionless_units(self): - """Tests for 'dimensonless' unit quantities""" + """Tests for 'dimensionless' unit quantities""" # Test some dimensionless units tests = { @@ -84,25 +98,20 @@ class ConversionTest(TestCase): for val, expected in tests.items(): # Convert, and leave units q = InvenTree.conversion.convert_physical_value(val, strip_units=False) - self.assertAlmostEqual(float(q.magnitude), expected, 0.01) + self.assertAlmostEqual(float(q.magnitude), expected, 3) # Convert, and strip units q = InvenTree.conversion.convert_physical_value(val) - self.assertAlmostEqual(q, expected, 0.01) + self.assertAlmostEqual(q, expected, 3) def test_invalid_values(self): """Test conversion of invalid inputs""" inputs = [ - '-', - ';;', '-x', - '?', - '--', - '+', - '++', '1/0', - '1/-', + 'xyz', + '12B45C' ] for val in inputs: @@ -112,8 +121,7 @@ class ConversionTest(TestCase): # Test dimensionless with self.assertRaises(ValidationError): - result = InvenTree.conversion.convert_physical_value(val) - print("Testing invalid value:", val, result) + InvenTree.conversion.convert_physical_value(val) def test_custom_units(self): """Tests for custom unit conversion""" @@ -154,11 +162,11 @@ class ConversionTest(TestCase): for val, expected in tests.items(): # Convert, and leave units q = InvenTree.conversion.convert_physical_value(val, 'henry / km', strip_units=False) - self.assertAlmostEqual(float(q.magnitude), expected, 0.01) + self.assertAlmostEqual(float(q.magnitude), expected, 2) # Convert and strip units q = InvenTree.conversion.convert_physical_value(val, 'henry / km') - self.assertAlmostEqual(q, expected, 0.01) + self.assertAlmostEqual(q, expected, 2) class ValidatorTest(TestCase): @@ -857,7 +865,7 @@ class CurrencyTests(TestCase): class TestStatus(TestCase): """Unit tests for status functions.""" - def test_check_system_healt(self): + def test_check_system_health(self): """Test that the system health check is false in testing -> background worker not running.""" self.assertEqual(status.check_system_health(), False) @@ -953,7 +961,7 @@ class TestSettings(InvenTreeTestCase): InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user) registry.reload_plugins(full_reload=True) - # Check that there was anotehr run + # Check that there was another run response = registry.install_plugin_file() self.assertEqual(response, True) @@ -1147,7 +1155,7 @@ class BarcodeMixinTest(InvenTreeTestCase): self.assertEqual(StockItem.barcode_model_type(), 'stockitem') self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation') - def test_bacode_hash(self): + def test_barcode_hash(self): """Test that the barcode hashing function provides correct results""" # Test multiple values for the hashing function @@ -1176,7 +1184,7 @@ class SanitizerTest(TestCase): # Test that valid string self.assertEqual(valid_string, sanitize_svg(valid_string)) - # Test that invalid string is cleanded + # Test that invalid string is cleaned self.assertNotEqual(dangerous_string, sanitize_svg(dangerous_string)) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 0ed067e56b..94e5c4400b 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -484,7 +484,7 @@ class PluginsRegistry: t_start = time.time() plg_i: InvenTreePlugin = plg() dt = time.time() - t_start - logger.info('Loaded plugin `%s` in %.3fs', plg_name, dt) + logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt) except Exception as error: handle_error(error, log_name='init') # log error and raise it -> disable plugin logger.warning("Plugin `%s` could not be loaded", plg_name) @@ -554,7 +554,7 @@ class PluginsRegistry: if hasattr(mixin, '_deactivate_mixin'): mixin._deactivate_mixin(self, force_reload=force_reload) - logger.info('Done deactivating') + logger.debug('Finished deactivating plugins') # endregion # region mixin specific loading ... diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index f5b4bbeda4..c9bd3dbd1a 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -257,6 +257,10 @@ function showApiError(xhr, url) { title = '{% trans "Error 408: Timeout" %}'; message = '{% trans "Connection timeout while requesting data from server" %}'; break; + case 503: + title = '{% trans "Error 503: Service Unavailable" %}'; + message = '{% trans "The server is currently unavailable" %}'; + break; default: title = '{% trans "Unhandled Error Code" %}'; message = `{% trans "Error code" %}: ${xhr.status}`;