mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-11 15:34:15 +00:00
Builtin plugins (#3889)
* Allow loading of "builtin" plugins, even if "plugins" are not explicitly loaded
* Updates for 'admin' buttons:
- Make them work like proper links
- Hidden if 'hide_admin_link' customization option is set
- Check for user staff status
* Cleanup rendering of "plugins" display
* Consolidate InvenTree barcode plugins into single plugin class
* Hide "install plugin" button if plugins are not enabled
* Add info message is external plugins are not enabled
* Fixes for loading plugins
- Always load 'builtin' plugins
- Refactor calls to "is_active" at various points in codebase
* Various tweaks
- Improve builtin plugin descriptions
- Spelling fixes
* Adjust plugin detail for builtin plugins
* Simplify barcode plugin class
* Simplify template rendering
* Bug fix for inventree barcode plugin
* Revert "Simplify template rendering"
This reverts commit 3a6755a659
.
* Re-re-improve template rendering
- Required as the template has been refactored for both "active" and "inactive" plugins
* Fixing unit tests for barcode plugin
* Ensure that barcode scan actions do not take a "long time":
- Add a default timeout of 0.1s to any POST or GET request in the testing framework
- Can be overridden by calling method if desired
* Display plugin "builtin" status in admin panel
* Fix unit tests for plugin API
* Further unit testing fixes
* Version number tweaks
* Further tweaks for unit testing
* Allow longer timeout for report printing via API
* Increase default timeout for API tests
- Sometimes CPU spike can cause the test to fail :|
* label printing can take a bit longer
* Remove timeout requirement from API tester
- Too variable to be reliable for CI
This commit is contained in:
@ -1,26 +0,0 @@
|
||||
"""Sample implementation for ActionMixin."""
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import ActionMixin
|
||||
|
||||
|
||||
class SimpleActionPlugin(ActionMixin, InvenTreePlugin):
|
||||
"""An EXTREMELY simple action plugin which demonstrates the capability of the ActionMixin class."""
|
||||
|
||||
NAME = "SimpleActionPlugin"
|
||||
ACTION_NAME = "simple"
|
||||
|
||||
def perform_action(self, user=None, data=None):
|
||||
"""Sample method."""
|
||||
print("Action plugin in action!")
|
||||
|
||||
def get_info(self, user, data=None):
|
||||
"""Sample method."""
|
||||
return {
|
||||
"user": user.username,
|
||||
"hello": "world",
|
||||
}
|
||||
|
||||
def get_result(self, user=None, data=None):
|
||||
"""Sample method."""
|
||||
return True
|
@ -1,36 +0,0 @@
|
||||
"""Unit tests for action plugins."""
|
||||
|
||||
from InvenTree.helpers import InvenTreeTestCase
|
||||
from plugin.builtin.action.simpleactionplugin import SimpleActionPlugin
|
||||
|
||||
|
||||
class SimpleActionPluginTests(InvenTreeTestCase):
|
||||
"""Tests for SampleIntegrationPlugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for tests."""
|
||||
super().setUp()
|
||||
|
||||
self.plugin = SimpleActionPlugin()
|
||||
|
||||
def test_name(self):
|
||||
"""Check plugn names."""
|
||||
self.assertEqual(self.plugin.plugin_name(), "SimpleActionPlugin")
|
||||
self.assertEqual(self.plugin.action_name(), "simple")
|
||||
|
||||
def test_function(self):
|
||||
"""Check if functions work."""
|
||||
# test functions
|
||||
response = self.client.post('/api/action/', data={'action': "simple", 'data': {'foo': "bar", }})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
str(response.content, encoding='utf8'),
|
||||
{
|
||||
"action": 'simple',
|
||||
"result": True,
|
||||
"info": {
|
||||
"user": self.username,
|
||||
"hello": "world",
|
||||
},
|
||||
}
|
||||
)
|
@ -9,6 +9,8 @@ references model objects actually exist in the database.
|
||||
|
||||
import json
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from company.models import SupplierPart
|
||||
from InvenTree.helpers import hash_barcode
|
||||
from part.models import Part
|
||||
@ -17,8 +19,14 @@ from plugin.mixins import BarcodeMixin
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
|
||||
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
"""Generic base class for handling InvenTree barcodes"""
|
||||
class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
"""Builtin BarcodePlugin for matching and generating internal barcodes."""
|
||||
|
||||
NAME = "InvenTreeBarcode"
|
||||
TITLE = _("Inventree Barcodes")
|
||||
DESCRIPTION = _("Provides native support for barcodes")
|
||||
VERSION = "2.0.0"
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
@staticmethod
|
||||
def get_supported_barcode_models():
|
||||
@ -58,58 +66,43 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin):
|
||||
"""Builtin BarcodePlugin for matching and generating internal barcodes."""
|
||||
|
||||
NAME = "InvenTreeInternalBarcode"
|
||||
|
||||
def scan(self, barcode_data):
|
||||
"""Scan a barcode against this plugin.
|
||||
|
||||
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
|
||||
"""
|
||||
|
||||
# Create hash from raw barcode data
|
||||
barcode_hash = hash_barcode(barcode_data)
|
||||
|
||||
# Attempt to coerce the barcode data into a dict object
|
||||
# This is the internal barcode representation that InvenTree uses
|
||||
barcode_dict = None
|
||||
|
||||
if type(barcode_data) is dict:
|
||||
pass
|
||||
barcode_dict = barcode_data
|
||||
elif type(barcode_data) is str:
|
||||
try:
|
||||
barcode_data = json.loads(barcode_data)
|
||||
barcode_dict = json.loads(barcode_data)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
pass
|
||||
|
||||
if type(barcode_data) is not dict:
|
||||
return None
|
||||
if barcode_dict is not None and type(barcode_dict) is dict:
|
||||
# Look for various matches. First good match will be returned
|
||||
for model in self.get_supported_barcode_models():
|
||||
label = model.barcode_model_type()
|
||||
|
||||
# Look for various matches. First good match will be returned
|
||||
for model in self.get_supported_barcode_models():
|
||||
label = model.barcode_model_type()
|
||||
if label in barcode_data:
|
||||
try:
|
||||
instance = model.objects.get(pk=barcode_data[label])
|
||||
return self.format_matched_response(label, model, instance)
|
||||
except (ValueError, model.DoesNotExist):
|
||||
pass
|
||||
|
||||
|
||||
class InvenTreeExternalBarcodePlugin(InvenTreeBarcodePlugin):
|
||||
"""Builtin BarcodePlugin for matching arbitrary external barcodes."""
|
||||
|
||||
NAME = "InvenTreeExternalBarcode"
|
||||
|
||||
def scan(self, barcode_data):
|
||||
"""Scan a barcode against this plugin.
|
||||
|
||||
Here we are looking for a dict object which contains a reference to a particular InvenTree databse object
|
||||
"""
|
||||
if label in barcode_dict:
|
||||
try:
|
||||
instance = model.objects.get(pk=barcode_dict[label])
|
||||
return self.format_matched_response(label, model, instance)
|
||||
except (ValueError, model.DoesNotExist):
|
||||
pass
|
||||
|
||||
# If no "direct" hits are found, look for assigned third-party barcodes
|
||||
for model in self.get_supported_barcode_models():
|
||||
label = model.barcode_model_type()
|
||||
|
||||
barcode_hash = hash_barcode(barcode_data)
|
||||
|
||||
instance = model.lookup_barcode(barcode_hash)
|
||||
|
||||
if instance is not None:
|
||||
|
@ -29,7 +29,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
'barcode': barcode_data,
|
||||
'stockitem': 521
|
||||
},
|
||||
expected_code=400
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
self.assertIn('error', response.data)
|
||||
@ -250,7 +250,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
self.assertIn('success', response.data)
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||
self.assertEqual(response.data['part']['pk'], 1)
|
||||
|
||||
# Attempting to assign the same barcode to a different part should result in an error
|
||||
@ -347,7 +347,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
response = self.scan({'barcode': 'blbla=10004'}, expected_code=200)
|
||||
|
||||
self.assertEqual(response.data['barcode_data'], 'blbla=10004')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||
|
||||
# Scan for a StockItem instance
|
||||
si = stock.models.StockItem.objects.get(pk=1)
|
||||
@ -402,7 +402,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['stocklocation']['pk'], 5)
|
||||
self.assertEqual(response.data['stocklocation']['api_url'], '/api/stock/location/5/')
|
||||
self.assertEqual(response.data['stocklocation']['web_url'], '/stock/location/5/')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||
|
||||
# Scan a Part object
|
||||
response = self.scan(
|
||||
@ -423,7 +423,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['supplierpart']['pk'], 1)
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
|
||||
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||
|
||||
self.assertIn('success', response.data)
|
||||
self.assertIn('barcode_data', response.data)
|
||||
|
@ -27,8 +27,10 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin):
|
||||
"""Core notification methods for InvenTree."""
|
||||
|
||||
NAME = "CoreNotificationsPlugin"
|
||||
TITLE = _("InvenTree Notifications")
|
||||
AUTHOR = _('InvenTree contributors')
|
||||
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
||||
VERSION = "1.0.0"
|
||||
|
||||
SETTINGS = {
|
||||
'ENABLE_NOTIFICATION_EMAILS': {
|
||||
|
Reference in New Issue
Block a user