From 8e962c0c59980a7382d9282d2a863ed05e148b1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Dec 2021 08:03:19 +0100 Subject: [PATCH 01/29] add mixin to consum a single API --- .../plugin/builtin/integration/mixins.py | 72 +++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 4 +- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 3a6b558db7..70345172ce 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -1,4 +1,7 @@ """default mixins for IntegrationMixins""" +import json +import requests + from django.conf.urls import url, include from plugin.urls import PLUGIN_BASE @@ -167,3 +170,72 @@ class AppMixin: this plugin is always an app with this plugin """ return True + + +class APICallMixin: + """Mixin that enables easier API calls for a plugin + + 1. Add this mixin + 2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`) + 3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING` + 4. Set `API_TOKEN` to the name required for the token / password by the external API + 5. (Optional) Override the `api_url` property method if some part of the APIs url is static + 6. (Optional) Override `api_headers` to add extra headers (by default the token/password and Content-Type are contained) + 6. Access the API in you plugin code via `api_call` + """ + API_METHOD = 'https' + API_URL_SETTING = None + API_PASSWORD_SETTING = None + + API_TOKEN = 'Bearer' + + class MixinMeta: + """meta options for this mixin""" + MIXIN_NAME = 'external API usage' + + def __init__(self): + super().__init__() + self.add_mixin('api_call', 'has_api_call', __class__) + + @property + def has_api_call(self): + """Is the mixin ready to call external APIs?""" + # TODO check if settings are set + return True + + @property + def api_url(self): + return f'{self.API_METHOD}://{self.get_globalsetting(self.API_URL_SETTING)}' + + @property + def api_headers(self): + return {self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), 'Content-Type': 'application/json'} + + def api_build_url_args(self, arguments): + groups = [] + for key, val in arguments.items(): + groups.append(f'{key}={",".join([str(a) for a in val])}') + return f'?{"&".join(groups)}' + + def api_call(self, endpoint, method: str='GET', url_args=None, data=None, headers=None, simple_response: bool = True): + if url_args: + endpoint += self.api_build_url_args(url_args) + + if headers is None: + headers = self.api_headers + + # build kwargs for call + kwargs = { + 'url': f'{self.api_url}/{endpoint}', + 'headers': headers, + } + if data: + kwargs['data'] = json.dumps(data) + + # run command + response = requests.request(method, **kwargs) + + # return + if simple_response: + return response.json() + return response diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index feb6bc3466..d63bae097b 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -1,6 +1,6 @@ """utility class to enable simpler imports""" -from ..builtin.integration.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin +from ..builtin.integration.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin, APICallMixin __all__ = [ - 'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin', + 'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin', 'APICallMixin', ] From 251fdeb69e161d1c03fb7751fcdab61c49319471 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Dec 2021 18:01:20 +0100 Subject: [PATCH 02/29] PEP fixes --- InvenTree/plugin/builtin/integration/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 70345172ce..932588c281 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -174,7 +174,7 @@ class AppMixin: class APICallMixin: """Mixin that enables easier API calls for a plugin - + 1. Add this mixin 2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`) 3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING` @@ -217,7 +217,7 @@ class APICallMixin: groups.append(f'{key}={",".join([str(a) for a in val])}') return f'?{"&".join(groups)}' - def api_call(self, endpoint, method: str='GET', url_args=None, data=None, headers=None, simple_response: bool = True): + def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True): if url_args: endpoint += self.api_build_url_args(url_args) From a2871ccb45beebf61d54ed30df91722646fa600d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Dec 2021 18:40:24 +0100 Subject: [PATCH 03/29] update database images before running --- .github/workflows/qc_checks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 929a299e93..4491ba7815 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -282,6 +282,7 @@ jobs: cache: 'pip' - name: Install Dependencies run: | + sudo apt-get update sudo apt-get install mysql-server libmysqlclient-dev pip3 install invoke pip3 install mysqlclient From 62394c4a826b06eaf37dfea8e90cc21021f507e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jan 2022 21:54:42 +0100 Subject: [PATCH 04/29] small reformat --- InvenTree/plugin/builtin/integration/mixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 457f86fb80..4f2d35268e 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -342,7 +342,10 @@ class APICallMixin: @property def api_headers(self): - return {self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), 'Content-Type': 'application/json'} + return { + self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), + 'Content-Type': 'application/json' + } def api_build_url_args(self, arguments): groups = [] From f59b59401fb66374a60555308b249411750b205e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jan 2022 21:58:44 +0100 Subject: [PATCH 05/29] refactor setting --- InvenTree/plugin/builtin/integration/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 4f2d35268e..590c7615d6 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -318,7 +318,7 @@ class APICallMixin: """ API_METHOD = 'https' API_URL_SETTING = None - API_PASSWORD_SETTING = None + API_TOKEN_SETTING = None API_TOKEN = 'Bearer' @@ -343,7 +343,7 @@ class APICallMixin: @property def api_headers(self): return { - self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), + self.API_TOKEN: self.get_globalsetting(self.API_TOKEN_SETTING), 'Content-Type': 'application/json' } From 3aea1bb7ba1c44c813ba8cf099d2ac43b91c5f62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jan 2022 21:59:02 +0100 Subject: [PATCH 06/29] made docstring clearer --- InvenTree/plugin/builtin/integration/mixins.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 590c7615d6..4001a100e3 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -306,14 +306,15 @@ class AppMixin: class APICallMixin: - """Mixin that enables easier API calls for a plugin + """ + Mixin that enables easier API calls for a plugin - 1. Add this mixin - 2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`) - 3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING` - 4. Set `API_TOKEN` to the name required for the token / password by the external API - 5. (Optional) Override the `api_url` property method if some part of the APIs url is static - 6. (Optional) Override `api_headers` to add extra headers (by default the token/password and Content-Type are contained) + 1. Add this mixin before (left of) SettingsMixin and PluginBase + 2. Add two settings for the required url and token/passowrd (use `SettingsMixin`) + 3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING` + 4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer` + 5. (Optional) Override the `api_url` property method if the setting needs to be extended + 6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained) 6. Access the API in you plugin code via `api_call` """ API_METHOD = 'https' From d939107d3633f623d76e83f140ff1a9852bee84b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:01:31 +0100 Subject: [PATCH 07/29] add example --- .../plugin/builtin/integration/mixins.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 4001a100e3..84d99d1bce 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -315,7 +315,40 @@ class APICallMixin: 4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer` 5. (Optional) Override the `api_url` property method if the setting needs to be extended 6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained) - 6. Access the API in you plugin code via `api_call` + 7. Access the API in you plugin code via `api_call` + + Example: + ``` + from plugin import IntegrationPluginBase + from plugin.mixins import APICallMixin, SettingsMixin + + + class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): + ''' + A small api call sample + ''' + PLUGIN_NAME = "Sample API Caller" + + SETTINGS = { + 'API_TOKEN': { + 'name': 'API Token', + 'protected': True, + }, + 'API_URL': { + 'name': 'External URL', + 'description': 'Where is your API located?', + 'default': 'https://reqres.in', + }, + } + API_URL_SETTING = 'API_URL' + API_TOKEN_SETTING = 'API_TOKEN' + + def get_external_url(self): + ''' + returns data from the sample endpoint + ''' + return self.api_call('api/users/2') + ``` """ API_METHOD = 'https' API_URL_SETTING = None From 33ee7e53dbbd861d32221b31969fa80d61a93b9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:01:50 +0100 Subject: [PATCH 08/29] append docstring --- InvenTree/plugin/builtin/integration/mixins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 84d99d1bce..d9f6bdc2d5 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -309,6 +309,7 @@ class APICallMixin: """ Mixin that enables easier API calls for a plugin + Steps to set up: 1. Add this mixin before (left of) SettingsMixin and PluginBase 2. Add two settings for the required url and token/passowrd (use `SettingsMixin`) 3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING` From 19f2c44c2a77e89bd508b99ceea45a2ce9233344 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:02:19 +0100 Subject: [PATCH 09/29] change mixin name --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index d9f6bdc2d5..22ba6de27d 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -359,7 +359,7 @@ class APICallMixin: class MixinMeta: """meta options for this mixin""" - MIXIN_NAME = 'external API usage' + MIXIN_NAME = 'API calls' def __init__(self): super().__init__() From 61b21d1ec14e0be683f8da2b92b3ca2aa9fdcf59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:03:05 +0100 Subject: [PATCH 10/29] add sample for api caller --- .../plugin/samples/integration/api_caller.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 InvenTree/plugin/samples/integration/api_caller.py diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py new file mode 100644 index 0000000000..eaef93a4b6 --- /dev/null +++ b/InvenTree/plugin/samples/integration/api_caller.py @@ -0,0 +1,34 @@ +""" +Sample plugin for calling an external API +""" +from django.utils.translation import ugettext_lazy as _ + +from plugin import IntegrationPluginBase +from plugin.mixins import APICallMixin, SettingsMixin + + +class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): + """ + A small api call sample + """ + PLUGIN_NAME = "Sample API Caller" + + SETTINGS = { + 'API_TOKEN': { + 'name': 'API Token', + 'protected': True, + }, + 'API_URL': { + 'name': 'External URL', + 'description': 'Where is your API located?', + 'default': 'https://reqres.in', + }, + } + API_URL_SETTING = 'API_URL' + API_TOKEN_SETTING = 'API_TOKEN' + + def get_external_url(self): + """ + returns data from the sample endpoint + """ + return self.api_call('api/users/2') From ed193e9e90304c04ed8047a2ffc41f8926808e0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:04:00 +0100 Subject: [PATCH 11/29] docstring for plugin base import class --- InvenTree/plugin/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index b3dc3a2fd0..86f65919c4 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -1,3 +1,7 @@ +""" +Utility file to enable simper imports +""" + from .registry import plugin_registry from .plugin import InvenTreePlugin from .integration import IntegrationPluginBase From ea8fd21af09933526a5930cce7c70b7e2b69eda4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:10:23 +0100 Subject: [PATCH 12/29] pip fix --- InvenTree/plugin/samples/integration/api_caller.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py index eaef93a4b6..7e5c883961 100644 --- a/InvenTree/plugin/samples/integration/api_caller.py +++ b/InvenTree/plugin/samples/integration/api_caller.py @@ -1,8 +1,6 @@ """ Sample plugin for calling an external API """ -from django.utils.translation import ugettext_lazy as _ - from plugin import IntegrationPluginBase from plugin.mixins import APICallMixin, SettingsMixin From b48e9bcac9cf8417cf51ba44f3f0577d2cc1fa89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:33:47 +0100 Subject: [PATCH 13/29] fix settings call --- InvenTree/plugin/builtin/integration/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 22ba6de27d..c8bb9f7f9e 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -373,12 +373,12 @@ class APICallMixin: @property def api_url(self): - return f'{self.API_METHOD}://{self.get_globalsetting(self.API_URL_SETTING)}' + return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}' @property def api_headers(self): return { - self.API_TOKEN: self.get_globalsetting(self.API_TOKEN_SETTING), + self.API_TOKEN: self.get_setting(self.API_TOKEN_SETTING), 'Content-Type': 'application/json' } From cc8948c708cdb00c28341e42c7b321f0ffa4d37b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:34:08 +0100 Subject: [PATCH 14/29] fix sample url --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- InvenTree/plugin/samples/integration/api_caller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index c8bb9f7f9e..66676d0520 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -338,7 +338,7 @@ class APICallMixin: 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'https://reqres.in', + 'default': 'reqres.in', }, } API_URL_SETTING = 'API_URL' diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py index 7e5c883961..36e1583ba0 100644 --- a/InvenTree/plugin/samples/integration/api_caller.py +++ b/InvenTree/plugin/samples/integration/api_caller.py @@ -19,7 +19,7 @@ class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'https://reqres.in', + 'default': 'reqres.in', }, } API_URL_SETTING = 'API_URL' From f9742ab41d6f75d44f853b7767b37f30fb289512 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:34:27 +0100 Subject: [PATCH 15/29] add integration test for plugin --- .../samples/integration/test_api_caller.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 InvenTree/plugin/samples/integration/test_api_caller.py diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py new file mode 100644 index 0000000000..7db431c3ee --- /dev/null +++ b/InvenTree/plugin/samples/integration/test_api_caller.py @@ -0,0 +1,20 @@ +""" Unit tests for action caller sample""" + +from django.test import TestCase + +from plugin import plugin_registry + +class SampleApiCallerPluginTests(TestCase): + """ Tests for SampleApiCallerPluginTests """ + + def test_return(self): + """check if the external api call works""" + # The plugin should be defined + self.assertIn('sample-api-caller', plugin_registry.plugins) + plg = plugin_registry.plugins['sample-api-caller'] + self.assertTrue(plg) + + # do an api call + result = plg.get_external_url() + self.assertTrue(result) + self.assertIn('data', result,) From ad9a9da656f7686dde4bd23133e2ea7b4cf63c50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 03:35:29 +0100 Subject: [PATCH 16/29] PEP fix --- InvenTree/plugin/samples/integration/test_api_caller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py index 7db431c3ee..e15edfad94 100644 --- a/InvenTree/plugin/samples/integration/test_api_caller.py +++ b/InvenTree/plugin/samples/integration/test_api_caller.py @@ -4,6 +4,7 @@ from django.test import TestCase from plugin import plugin_registry + class SampleApiCallerPluginTests(TestCase): """ Tests for SampleApiCallerPluginTests """ From 3e2e9aaf9ec49cdd6b28faf626767ded42669aeb Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 9 Jan 2022 20:10:00 +1100 Subject: [PATCH 17/29] Mark serializer fields as not required --- InvenTree/build/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 55f89c1844..01ea9fd924 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -248,6 +248,8 @@ class BuildCompleteSerializer(serializers.Serializer): accept_unallocated = serializers.BooleanField( label=_('Accept Unallocated'), help_text=_('Accept that stock items have not been fully allocated to this build order'), + required=False, + default=False, ) def validate_accept_unallocated(self, value): @@ -262,6 +264,8 @@ class BuildCompleteSerializer(serializers.Serializer): accept_incomplete = serializers.BooleanField( label=_('Accept Incomplete'), help_text=_('Accept that the required number of build outputs have not been completed'), + required=False, + default=False, ) def validate_accept_incomplete(self, value): From da9fa1313c8fb91b17f3e75f87f8f0e765a697f2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 9 Jan 2022 22:14:48 +1100 Subject: [PATCH 18/29] Increased unit testing --- InvenTree/build/models.py | 2 +- InvenTree/build/serializers.py | 9 ++++++++ InvenTree/build/test_api.py | 42 ++++++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f03cb30c74..cd8f4df16f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -582,7 +582,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): self.subtractUntrackedStock(user) # Ensure that there are no longer any BuildItem objects - # which point to thie Build Order + # which point to thisFcan Build Order self.allocated_stock.all().delete() @transaction.atomic diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 01ea9fd924..04cbc162c9 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -277,6 +277,15 @@ class BuildCompleteSerializer(serializers.Serializer): return value + def validate(self, data): + + build = self.context['build'] + + if build.incomplete_count > 0: + raise ValidationError(_("Build order has incomplete outputs")) + + return data + def save(self): request = self.context['request'] diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index 45662a58d6..64d17430a0 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -38,7 +38,7 @@ class BuildAPITest(InvenTreeAPITestCase): super().setUp() -class BuildCompleteTest(BuildAPITest): +class BuildOutputCompleteTest(BuildAPITest): """ Unit testing for the build complete API endpoint """ @@ -140,6 +140,9 @@ class BuildCompleteTest(BuildAPITest): Test build order completion """ + # Initially, build should not be able to be completed + self.assertFalse(self.build.can_complete) + # We start without any outputs assigned against the build self.assertEqual(self.build.incomplete_outputs.count(), 0) @@ -153,7 +156,7 @@ class BuildCompleteTest(BuildAPITest): self.assertEqual(self.build.completed, 0) # We shall complete 4 of these outputs - outputs = self.build.incomplete_outputs[0:4] + outputs = self.build.incomplete_outputs.all() self.post( self.url, @@ -165,19 +168,44 @@ class BuildCompleteTest(BuildAPITest): expected_code=201 ) - # There now should be 6 incomplete build outputs remaining - self.assertEqual(self.build.incomplete_outputs.count(), 6) + self.assertEqual(self.build.incomplete_outputs.count(), 0) - # And there should be 4 completed outputs + # And there should be 10 completed outputs outputs = self.build.complete_outputs - self.assertEqual(outputs.count(), 4) + self.assertEqual(outputs.count(), 10) for output in outputs: self.assertFalse(output.is_building) self.assertEqual(output.build, self.build) self.build.refresh_from_db() - self.assertEqual(self.build.completed, 40) + self.assertEqual(self.build.completed, 100) + + # Try to complete the build (it should fail) + finish_url = reverse('api-build-finish', kwargs={'pk': self.build.pk}) + + response = self.post( + finish_url, + {}, + expected_code=400 + ) + + self.assertTrue('accept_unallocated' in response.data) + + # Accept unallocated stock + response = self.post( + finish_url, + { + 'accept_unallocated': True, + }, + expected_code=201, + ) + + # Accept unfinished + self.build.refresh_from_db() + + # Build should have been marked as complete + self.assertTrue(self.build.is_complete) class BuildAllocationTest(BuildAPITest): From 6aa83796eadb029e8635f6b18df0d17bda3cf24e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 9 Jan 2022 22:15:59 +1100 Subject: [PATCH 19/29] PEP fixes --- InvenTree/build/serializers.py | 2 +- InvenTree/build/test_api.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 04cbc162c9..fb34a40a16 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -282,7 +282,7 @@ class BuildCompleteSerializer(serializers.Serializer): build = self.context['build'] if build.incomplete_count > 0: - raise ValidationError(_("Build order has incomplete outputs")) + raise ValidationError(_("Build order has incomplete outputs")) return data diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index 64d17430a0..1e0c2b4792 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -201,7 +201,6 @@ class BuildOutputCompleteTest(BuildAPITest): expected_code=201, ) - # Accept unfinished self.build.refresh_from_db() # Build should have been marked as complete From c699ced34ad263fdb0cdc6372d73fa2e5a7ac7a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 22:16:19 +0100 Subject: [PATCH 20/29] make general mixin tests multi mixin enabled --- InvenTree/plugin/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py index 3d88fed4dd..abddc9ee59 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_integration.py @@ -15,9 +15,9 @@ from plugin.urls import PLUGIN_BASE class BaseMixinDefinition: def test_mixin_name(self): # mixin name - self.assertEqual(self.mixin.registered_mixins[0]['key'], self.MIXIN_NAME) + self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins]) # human name - self.assertEqual(self.mixin.registered_mixins[0]['human_name'], self.MIXIN_HUMAN_NAME) + self.assertIn(self.MIXIN_HUMAN_NAME, [item['human_name'] for item in self.mixin.registered_mixins]) class SettingsMixinTest(BaseMixinDefinition, TestCase): From 31d587a9b190ef7eb0c7546fa6e4ed25e477da3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 22:19:01 +0100 Subject: [PATCH 21/29] unittests fdor ApiCallMixin --- InvenTree/plugin/test_integration.py | 46 +++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py index abddc9ee59..eb55f984d3 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_integration.py @@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model from datetime import datetime from plugin import IntegrationPluginBase -from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin +from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE @@ -142,6 +142,50 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(self.nothing_mixin.navigation_name, '') +class APICallMixinTest(BaseMixinDefinition, TestCase): + MIXIN_HUMAN_NAME = 'API calls' + MIXIN_NAME = 'api_call' + MIXIN_ENABLE_CHECK = 'has_api_call' + + def setUp(self): + class MixinCls(APICallMixin, SettingsMixin, IntegrationPluginBase): + PLUGIN_NAME = "Sample API Caller" + + SETTINGS = { + 'API_TOKEN': { + 'name': 'API Token', + 'protected': True, + }, + 'API_URL': { + 'name': 'External URL', + 'description': 'Where is your API located?', + 'default': 'reqres.in', + }, + } + API_URL_SETTING = 'API_URL' + API_TOKEN_SETTING = 'API_TOKEN' + + def get_external_url(self): + ''' + returns data from the sample endpoint + ''' + return self.api_call('api/users/2') + self.mixin = MixinCls() + def test_function(self): + # api_url + self.assertEqual('https://reqres.in', self.mixin.api_url) + + # api_headers + headers = self.mixin.api_headers + self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'}) + + # api_build_url_args + # api_call + result = self.mixin.get_external_url() + self.assertTrue(result) + self.assertIn('data', result,) + + class IntegrationPluginBaseTests(TestCase): """ Tests for IntegrationPluginBase """ From e889f487f010be2b59a0961e051ce93e54db40de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 22:27:50 +0100 Subject: [PATCH 22/29] added a check for the required constants --- InvenTree/plugin/builtin/integration/mixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 66676d0520..7d257dbee5 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -368,7 +368,10 @@ class APICallMixin: @property def has_api_call(self): """Is the mixin ready to call external APIs?""" - # TODO check if settings are set + if not bool(self.API_URL_SETTING): + raise ValueError("API_URL_SETTING must be defined") + if not bool(self.API_TOKEN_SETTING): + raise ValueError("API_TOKEN_SETTING must be defined") return True @property From c8599039a25cb17535f41149d588ff3acb4836af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 22:33:14 +0100 Subject: [PATCH 23/29] added test for wrong config --- InvenTree/plugin/test_integration.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py index eb55f984d3..48533a70e2 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_integration.py @@ -171,6 +171,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): ''' return self.api_call('api/users/2') self.mixin = MixinCls() + + class WrongCLS(APICallMixin, IntegrationPluginBase): + pass + self.mixin_nothing = WrongCLS() + def test_function(self): # api_url self.assertEqual('https://reqres.in', self.mixin.api_url) @@ -185,6 +190,10 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertIn('data', result,) + # wrongly defined plugins should not load + with self.assertRaises(ValueError): + self.mixin_nothing.has_api_call() + class IntegrationPluginBaseTests(TestCase): """ Tests for IntegrationPluginBase """ From 2c05b858a4afa2542a20556b6a5f3b6c8ea6f9e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Jan 2022 22:34:02 +0100 Subject: [PATCH 24/29] renmae var --- InvenTree/plugin/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py index 48533a70e2..a65fc85708 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_integration.py @@ -174,7 +174,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): class WrongCLS(APICallMixin, IntegrationPluginBase): pass - self.mixin_nothing = WrongCLS() + self.mixin_wrong = WrongCLS() def test_function(self): # api_url @@ -192,7 +192,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # wrongly defined plugins should not load with self.assertRaises(ValueError): - self.mixin_nothing.has_api_call() + self.mixin_wrong.has_api_call() class IntegrationPluginBaseTests(TestCase): From f219a10b2f528f49e90bc91734d2e0aa41c29575 Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Sun, 9 Jan 2022 22:46:28 +0100 Subject: [PATCH 25/29] small fix to language in readme normally the kind of API we supply is called REST --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bb4152657..324bd7a7a1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree) ![CI](https://github.com/inventree/inventree/actions/workflows/qc_checks.yaml/badge.svg) -InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications. +InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a REST API for interaction with external interfaces and applications. InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions. From 0002edc32ce9ad37b60719586081a501123201b0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 10 Jan 2022 08:50:50 +1100 Subject: [PATCH 26/29] Hide "restart required" message in demo mode --- InvenTree/templates/base.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index a71f4e67c9..fd3e496f80 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -6,6 +6,7 @@ {% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %} {% settings_value "REPORT_ENABLE" as report_enabled %} {% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %} +{% inventree_demo_mode as demo_mode %} @@ -90,7 +91,7 @@ {% block alerts %}
- {% if server_restart_required %} + {% if server_restart_required and not demo_mode %}