From 0e0ba66b9a2c87a85c889872c1ddda860840fbdd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 21:40:53 +1000 Subject: [PATCH 1/3] Fix broken calls to offload_task --- InvenTree/plugin/base/integration/mixins.py | 3 ++- InvenTree/plugin/base/locate/api.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 86e3092e4f..64de5df22b 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -13,6 +13,7 @@ import InvenTree.helpers from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template from plugin.models import PluginConfig, PluginSetting +from plugin.registry import registry from plugin.urls import PLUGIN_BASE @@ -204,7 +205,7 @@ class ScheduleMixin: Schedule.objects.create( name=task_name, - func='plugin.registry.call_function', + func=registry.call_plugin_function, args=f"'{slug}', '{func_name}'", schedule_type=task['schedule'], minutes=task.get('minutes', None), diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index a6776f2d40..f617ba3577 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -7,7 +7,7 @@ from rest_framework.views import APIView from InvenTree.tasks import offload_task -from plugin import registry +from plugin.registry import registry from stock.models import StockItem, StockLocation @@ -53,7 +53,7 @@ class LocatePluginView(APIView): try: StockItem.objects.get(pk=item_pk) - offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk) + offload_task(registry.call_plugin_function, plugin, 'locate_stock_item', item_pk) data['item'] = item_pk @@ -66,7 +66,7 @@ class LocatePluginView(APIView): try: StockLocation.objects.get(pk=location_pk) - offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk) + offload_task(registry.call_plugin_function, plugin, 'locate_stock_location', location_pk) data['location'] = location_pk From dd476ce796103f2a8fe84a0be240604249a060a8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 22:20:29 +1000 Subject: [PATCH 2/3] Add unit tests for the 'locate' plugin - Test various failure modes - Some of the failure modes didn't fail - this is also a failure - Fixing API code accordingly --- InvenTree/plugin/base/locate/api.py | 14 ++-- InvenTree/plugin/base/locate/test_locate.py | 89 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 InvenTree/plugin/base/locate/test_locate.py diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index f617ba3577..a7effe91b3 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -40,9 +40,6 @@ class LocatePluginView(APIView): # StockLocation to identify location_pk = request.data.get('location', None) - if not item_pk and not location_pk: - raise ParseError("Must supply either 'item' or 'location' parameter") - data = { "success": "Identification plugin activated", "plugin": plugin, @@ -59,8 +56,8 @@ class LocatePluginView(APIView): return Response(data) - except StockItem.DoesNotExist: - raise NotFound("StockItem matching PK '{item}' not found") + except (ValueError, StockItem.DoesNotExist): + raise NotFound(f"StockItem matching PK '{item_pk}' not found") elif location_pk: try: @@ -72,8 +69,9 @@ class LocatePluginView(APIView): return Response(data) - except StockLocation.DoesNotExist: - raise NotFound("StockLocation matching PK {'location'} not found") + except (ValueError, StockLocation.DoesNotExist): + raise NotFound(f"StockLocation matching PK '{location_pk}' not found") else: - raise NotFound() + raise ParseError("Must supply either 'item' or 'location' parameter") + diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py new file mode 100644 index 0000000000..26fafcca49 --- /dev/null +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -0,0 +1,89 @@ +""" +Unit tests for the 'locate' plugin mixin class +""" + +from django.urls import reverse + +from InvenTree.api_tester import InvenTreeAPITestCase + +from plugin.registry import registry + + +class LocatePluginTests(InvenTreeAPITestCase): + + fixtures = [ + 'category', + 'part', + 'location', + 'stock', + ] + + def test_installed(self): + """Test that a locate plugin is actually installed""" + + plugins = registry.with_mixin('locate') + + self.assertTrue(len(plugins) > 0) + + self.assertTrue('samplelocate' in [p.slug for p in plugins]) + + def test_locate_fail(self): + """Test various API failure modes""" + + url = reverse('api-locate-plugin') + + # Post without a plugin + response = self.post( + url, + {}, + expected_code=400 + ) + + self.assertIn("'plugin' field must be supplied", str(response.data)) + + # Post with a plugin that does not exist, or is invalid + for slug in ['xyz', 'event', 'plugin']: + response = self.post( + url, + { + 'plugin': slug, + }, + expected_code=400, + ) + + self.assertIn(f"Plugin '{slug}' is not installed, or does not support the location mixin", str(response.data)) + + # Post with a valid plugin, but no other data + response = self.post( + url, + { + 'plugin': 'samplelocate', + }, + expected_code=400 + ) + + self.assertIn("Must supply either 'item' or 'location' parameter", str(response.data)) + + # Post with valid plugin, invalid item or location + for pk in ['qq', 99999, -42]: + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'item': pk, + }, + expected_code=404 + ) + + self.assertIn(f"StockItem matching PK '{pk}' not found", str(response.data)) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'location': pk, + }, + expected_code=404, + ) + + self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) \ No newline at end of file From c6590066b865416e5761718e2a61dca06ad44e81 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 May 2022 22:46:15 +1000 Subject: [PATCH 3/3] Add tests for successful location - Sample plugin now updates metadata tag --- InvenTree/plugin/base/locate/api.py | 1 - InvenTree/plugin/base/locate/test_locate.py | 63 ++++++++++++++++++- .../plugin/samples/locate/locate_sample.py | 24 ++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index a7effe91b3..3004abb262 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -74,4 +74,3 @@ class LocatePluginView(APIView): else: raise ParseError("Must supply either 'item' or 'location' parameter") - diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py index 26fafcca49..e145c2360b 100644 --- a/InvenTree/plugin/base/locate/test_locate.py +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -7,6 +7,7 @@ from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase from plugin.registry import registry +from stock.models import StockItem, StockLocation class LocatePluginTests(InvenTreeAPITestCase): @@ -29,7 +30,7 @@ class LocatePluginTests(InvenTreeAPITestCase): def test_locate_fail(self): """Test various API failure modes""" - + url = reverse('api-locate-plugin') # Post without a plugin @@ -86,4 +87,62 @@ class LocatePluginTests(InvenTreeAPITestCase): expected_code=404, ) - self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) \ No newline at end of file + self.assertIn(f"StockLocation matching PK '{pk}' not found", str(response.data)) + + def test_locate_item(self): + """ + Test that the plugin correctly 'locates' a StockItem + + As the background worker is not running during unit testing, + the sample 'locate' function will be called 'inline' + """ + + url = reverse('api-locate-plugin') + + item = StockItem.objects.get(pk=1) + + # The sample plugin will set the 'located' metadata tag + item.set_metadata('located', False) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'item': 1, + }, + expected_code=200 + ) + + self.assertEqual(response.data['item'], 1) + + item.refresh_from_db() + + # Item metadata should have been altered! + self.assertTrue(item.metadata['located']) + + def test_locate_location(self): + """ + Test that the plugin correctly 'locates' a StockLocation + """ + + url = reverse('api-locate-plugin') + + for location in StockLocation.objects.all(): + + location.set_metadata('located', False) + + response = self.post( + url, + { + 'plugin': 'samplelocate', + 'location': location.pk, + }, + expected_code=200 + ) + + self.assertEqual(response.data['location'], location.pk) + + location.refresh_from_db() + + # Item metadata should have been altered! + self.assertTrue(location.metadata['located']) diff --git a/InvenTree/plugin/samples/locate/locate_sample.py b/InvenTree/plugin/samples/locate/locate_sample.py index 458b84cfa5..32a2dd713c 100644 --- a/InvenTree/plugin/samples/locate/locate_sample.py +++ b/InvenTree/plugin/samples/locate/locate_sample.py @@ -23,7 +23,23 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): SLUG = "samplelocate" TITLE = "Sample plugin for locating items" - VERSION = "0.1" + VERSION = "0.2" + + def locate_stock_item(self, item_pk): + + from stock.models import StockItem + + logger.info(f"SampleLocatePlugin attempting to locate item ID {item_pk}") + + try: + item = StockItem.objects.get(pk=item_pk) + logger.info(f"StockItem {item_pk} located!") + + # Tag metadata + item.set_metadata('located', True) + + except (ValueError, StockItem.DoesNotExist): + logger.error(f"StockItem ID {item_pk} does not exist!") def locate_stock_location(self, location_pk): @@ -34,5 +50,9 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): try: location = StockLocation.objects.get(pk=location_pk) logger.info(f"Location exists at '{location.pathstring}'") - except StockLocation.DoesNotExist: + + # Tag metadata + location.set_metadata('located', True) + + except (ValueError, StockLocation.DoesNotExist): logger.error(f"Location ID {location_pk} does not exist!")