mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +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 3a6755a659fe92963b8bee8378638e28271dd369. * 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:
parent
fe1b8cbfce
commit
b50a6826ef
@ -134,7 +134,7 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
|||||||
if expected_code is not None:
|
if expected_code is not None:
|
||||||
|
|
||||||
if response.status_code != expected_code:
|
if response.status_code != expected_code:
|
||||||
print(f"Unexpected response at '{url}':")
|
print(f"Unexpected response at '{url}': status_code = {response.status_code}")
|
||||||
print(response.data)
|
print(response.data)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, expected_code)
|
self.assertEqual(response.status_code, expected_code)
|
||||||
@ -143,11 +143,13 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
|||||||
|
|
||||||
def post(self, url, data=None, expected_code=None, format='json'):
|
def post(self, url, data=None, expected_code=None, format='json'):
|
||||||
"""Issue a POST request."""
|
"""Issue a POST request."""
|
||||||
response = self.client.post(url, data=data, format=format)
|
|
||||||
|
|
||||||
|
# Set default value - see B006
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
|
response = self.client.post(url, data=data, format=format)
|
||||||
|
|
||||||
if expected_code is not None:
|
if expected_code is not None:
|
||||||
|
|
||||||
if response.status_code != expected_code:
|
if response.status_code != expected_code:
|
||||||
|
@ -127,13 +127,6 @@ function inventreeDocReady() {
|
|||||||
loadBrandIcon($(this), $(this).attr('brand_name'));
|
loadBrandIcon($(this), $(this).attr('brand_name'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback for "admin view" button
|
|
||||||
$('#admin-button, .admin-button').click(function() {
|
|
||||||
var url = $(this).attr('url');
|
|
||||||
|
|
||||||
location.href = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display any cached alert messages
|
// Display any cached alert messages
|
||||||
showCachedAlerts();
|
showCachedAlerts();
|
||||||
|
|
||||||
|
@ -178,11 +178,15 @@ class APITests(InvenTreeAPITestCase):
|
|||||||
def test_with_roles(self):
|
def test_with_roles(self):
|
||||||
"""Assign some roles to the user."""
|
"""Assign some roles to the user."""
|
||||||
self.basicAuth()
|
self.basicAuth()
|
||||||
response = self.get(reverse('api-user-roles'))
|
|
||||||
|
url = reverse('api-user-roles')
|
||||||
|
|
||||||
|
response = self.get(url)
|
||||||
|
|
||||||
self.assignRole('part.delete')
|
self.assignRole('part.delete')
|
||||||
self.assignRole('build.change')
|
self.assignRole('build.change')
|
||||||
response = self.get(reverse('api-user-roles'))
|
|
||||||
|
response = self.get(url)
|
||||||
|
|
||||||
roles = response.data['roles']
|
roles = response.data['roles']
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ class BuildTest(BuildAPITest):
|
|||||||
"location": 1,
|
"location": 1,
|
||||||
"status": 50, # Item requires attention
|
"status": 50, # Item requires attention
|
||||||
},
|
},
|
||||||
expected_code=201
|
expected_code=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||||
|
@ -1332,7 +1332,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
'PLUGIN_ON_STARTUP': {
|
'PLUGIN_ON_STARTUP': {
|
||||||
'name': _('Check plugins on startup'),
|
'name': _('Check plugins on startup'),
|
||||||
'description': _('Check that all plugins are installed on startup - enable in container enviroments'),
|
'description': _('Check that all plugins are installed on startup - enable in container environments'),
|
||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
'requires_restart': True,
|
'requires_restart': True,
|
||||||
|
@ -67,9 +67,7 @@ class LabelPrintMixin:
|
|||||||
plugin = registry.get_plugin(plugin_key)
|
plugin = registry.get_plugin(plugin_key)
|
||||||
|
|
||||||
if plugin:
|
if plugin:
|
||||||
config = plugin.plugin_config()
|
if plugin.is_active():
|
||||||
|
|
||||||
if config and config.active:
|
|
||||||
# Only return the plugin if it is enabled!
|
# Only return the plugin if it is enabled!
|
||||||
return plugin
|
return plugin
|
||||||
else:
|
else:
|
||||||
|
@ -593,7 +593,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
{
|
{
|
||||||
'convert_from': variant.pk,
|
'convert_from': variant.pk,
|
||||||
},
|
},
|
||||||
expected_code=200
|
expected_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
# There should be the same number of results for each request
|
# There should be the same number of results for each request
|
||||||
@ -1854,7 +1854,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
data={
|
data={
|
||||||
'validated': True,
|
'validated': True,
|
||||||
},
|
},
|
||||||
expected_code=200
|
expected_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the expected response is returned
|
# Check that the expected response is returned
|
||||||
|
@ -52,7 +52,7 @@ class PluginConfigAdmin(admin.ModelAdmin):
|
|||||||
"""Custom admin with restricted id fields."""
|
"""Custom admin with restricted id fields."""
|
||||||
|
|
||||||
readonly_fields = ["key", "name", ]
|
readonly_fields = ["key", "name", ]
|
||||||
list_display = ['name', 'key', '__str__', 'active', 'is_sample']
|
list_display = ['name', 'key', '__str__', 'active', 'is_builtin', 'is_sample']
|
||||||
list_filter = ['active']
|
list_filter = ['active']
|
||||||
actions = [plugin_activate, plugin_deactivate, ]
|
actions = [plugin_activate, plugin_deactivate, ]
|
||||||
inlines = [PluginSettingInline, ]
|
inlines = [PluginSettingInline, ]
|
||||||
|
@ -7,7 +7,6 @@ The main code for plugin special sauce is in the plugin registry in `InvenTree/p
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from maintenance_mode.core import set_maintenance_mode
|
from maintenance_mode.core import set_maintenance_mode
|
||||||
@ -26,7 +25,6 @@ class PluginAppConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
"""The ready method is extended to initialize plugins."""
|
"""The ready method is extended to initialize plugins."""
|
||||||
if settings.PLUGINS_ENABLED:
|
|
||||||
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
|
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
|
||||||
logger.info("Skipping plugin loading sequence") # pragma: no cover
|
logger.info("Skipping plugin loading sequence") # pragma: no cover
|
||||||
else:
|
else:
|
||||||
@ -52,8 +50,9 @@ class PluginAppConfig(AppConfig):
|
|||||||
|
|
||||||
# check git version
|
# check git version
|
||||||
registry.git_is_modern = check_git_version()
|
registry.git_is_modern = check_git_version()
|
||||||
|
|
||||||
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
||||||
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
log_error(_('Your environment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info("Plugins not enabled - skipping loading sequence") # pragma: no cover
|
logger.info("Plugins not enabled - skipping loading sequence") # pragma: no cover
|
||||||
|
@ -11,8 +11,8 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
from InvenTree.helpers import hash_barcode
|
from InvenTree.helpers import hash_barcode
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
from plugin.builtin.barcodes.inventree_barcode import (
|
from plugin.builtin.barcodes.inventree_barcode import \
|
||||||
InvenTreeExternalBarcodePlugin, InvenTreeInternalBarcodePlugin)
|
InvenTreeInternalBarcodePlugin
|
||||||
from users.models import RuleSet
|
from users.models import RuleSet
|
||||||
|
|
||||||
|
|
||||||
@ -53,11 +53,8 @@ class BarcodeScan(APIView):
|
|||||||
if not barcode_data:
|
if not barcode_data:
|
||||||
raise ValidationError({'barcode': _('Missing barcode data')})
|
raise ValidationError({'barcode': _('Missing barcode data')})
|
||||||
|
|
||||||
# Ensure that the default barcode handlers are run first
|
# Note: the default barcode handlers are loaded (and thus run) first
|
||||||
plugins = [
|
plugins = registry.with_mixin('barcode')
|
||||||
InvenTreeInternalBarcodePlugin(),
|
|
||||||
InvenTreeExternalBarcodePlugin(),
|
|
||||||
] + registry.with_mixin('barcode')
|
|
||||||
|
|
||||||
barcode_hash = hash_barcode(barcode_data)
|
barcode_hash = hash_barcode(barcode_data)
|
||||||
|
|
||||||
@ -113,10 +110,7 @@ class BarcodeAssign(APIView):
|
|||||||
raise ValidationError({'barcode': _('Missing barcode data')})
|
raise ValidationError({'barcode': _('Missing barcode data')})
|
||||||
|
|
||||||
# Here we only check against 'InvenTree' plugins
|
# Here we only check against 'InvenTree' plugins
|
||||||
plugins = [
|
plugins = registry.with_mixin('barcode', builtin=True)
|
||||||
InvenTreeInternalBarcodePlugin(),
|
|
||||||
InvenTreeExternalBarcodePlugin(),
|
|
||||||
]
|
|
||||||
|
|
||||||
# First check if the provided barcode matches an existing database entry
|
# First check if the provided barcode matches an existing database entry
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
@ -133,7 +127,7 @@ class BarcodeAssign(APIView):
|
|||||||
|
|
||||||
valid_labels = []
|
valid_labels = []
|
||||||
|
|
||||||
for model in InvenTreeExternalBarcodePlugin.get_supported_barcode_models():
|
for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models():
|
||||||
label = model.barcode_model_type()
|
label = model.barcode_model_type()
|
||||||
valid_labels.append(label)
|
valid_labels.append(label)
|
||||||
|
|
||||||
@ -188,7 +182,7 @@ class BarcodeUnassign(APIView):
|
|||||||
"""Respond to a barcode unassign POST request"""
|
"""Respond to a barcode unassign POST request"""
|
||||||
|
|
||||||
# The following database models support assignment of third-party barcodes
|
# The following database models support assignment of third-party barcodes
|
||||||
supported_models = InvenTreeExternalBarcodePlugin.get_supported_barcode_models()
|
supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
|
||||||
|
|
||||||
supported_labels = [model.barcode_model_type() for model in supported_models]
|
supported_labels = [model.barcode_model_type() for model in supported_models]
|
||||||
model_names = ', '.join(supported_labels)
|
model_names = ', '.join(supported_labels)
|
||||||
|
@ -58,9 +58,8 @@ def register_event(event, *args, **kwargs):
|
|||||||
|
|
||||||
if plugin.mixin_enabled('events'):
|
if plugin.mixin_enabled('events'):
|
||||||
|
|
||||||
config = plugin.plugin_config()
|
if plugin.is_active():
|
||||||
|
# Only allow event registering for 'active' plugins
|
||||||
if config and config.active:
|
|
||||||
|
|
||||||
logger.debug(f"Registering callback for plugin '{slug}'")
|
logger.debug(f"Registering callback for plugin '{slug}'")
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ references model objects actually exist in the database.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree.helpers import hash_barcode
|
from InvenTree.helpers import hash_barcode
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -17,8 +19,14 @@ from plugin.mixins import BarcodeMixin
|
|||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||||
"""Generic base class for handling InvenTree barcodes"""
|
"""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
|
@staticmethod
|
||||||
def get_supported_barcode_models():
|
def get_supported_barcode_models():
|
||||||
@ -58,58 +66,43 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin):
|
|
||||||
"""Builtin BarcodePlugin for matching and generating internal barcodes."""
|
|
||||||
|
|
||||||
NAME = "InvenTreeInternalBarcode"
|
|
||||||
|
|
||||||
def scan(self, barcode_data):
|
def scan(self, barcode_data):
|
||||||
"""Scan a barcode against this plugin.
|
"""Scan a barcode against this plugin.
|
||||||
|
|
||||||
Here we are looking for a dict object which contains a reference to a particular InvenTree database object
|
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:
|
if type(barcode_data) is dict:
|
||||||
pass
|
barcode_dict = barcode_data
|
||||||
elif type(barcode_data) is str:
|
elif type(barcode_data) is str:
|
||||||
try:
|
try:
|
||||||
barcode_data = json.loads(barcode_data)
|
barcode_dict = json.loads(barcode_data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return None
|
pass
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
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
|
# Look for various matches. First good match will be returned
|
||||||
for model in self.get_supported_barcode_models():
|
for model in self.get_supported_barcode_models():
|
||||||
label = model.barcode_model_type()
|
label = model.barcode_model_type()
|
||||||
if label in barcode_data:
|
|
||||||
|
if label in barcode_dict:
|
||||||
try:
|
try:
|
||||||
instance = model.objects.get(pk=barcode_data[label])
|
instance = model.objects.get(pk=barcode_dict[label])
|
||||||
return self.format_matched_response(label, model, instance)
|
return self.format_matched_response(label, model, instance)
|
||||||
except (ValueError, model.DoesNotExist):
|
except (ValueError, model.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# If no "direct" hits are found, look for assigned third-party barcodes
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
for model in self.get_supported_barcode_models():
|
for model in self.get_supported_barcode_models():
|
||||||
label = model.barcode_model_type()
|
label = model.barcode_model_type()
|
||||||
|
|
||||||
barcode_hash = hash_barcode(barcode_data)
|
|
||||||
|
|
||||||
instance = model.lookup_barcode(barcode_hash)
|
instance = model.lookup_barcode(barcode_hash)
|
||||||
|
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
|
@ -29,7 +29,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
'barcode': barcode_data,
|
'barcode': barcode_data,
|
||||||
'stockitem': 521
|
'stockitem': 521
|
||||||
},
|
},
|
||||||
expected_code=400
|
expected_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn('error', response.data)
|
self.assertIn('error', response.data)
|
||||||
@ -250,7 +250,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn('success', response.data)
|
self.assertIn('success', response.data)
|
||||||
self.assertEqual(response.data['plugin'], 'InvenTreeExternalBarcode')
|
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||||
self.assertEqual(response.data['part']['pk'], 1)
|
self.assertEqual(response.data['part']['pk'], 1)
|
||||||
|
|
||||||
# Attempting to assign the same barcode to a different part should result in an error
|
# 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)
|
response = self.scan({'barcode': 'blbla=10004'}, expected_code=200)
|
||||||
|
|
||||||
self.assertEqual(response.data['barcode_data'], 'blbla=10004')
|
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
|
# Scan for a StockItem instance
|
||||||
si = stock.models.StockItem.objects.get(pk=1)
|
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']['pk'], 5)
|
||||||
self.assertEqual(response.data['stocklocation']['api_url'], '/api/stock/location/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['stocklocation']['web_url'], '/stock/location/5/')
|
||||||
self.assertEqual(response.data['plugin'], 'InvenTreeInternalBarcode')
|
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
|
||||||
|
|
||||||
# Scan a Part object
|
# Scan a Part object
|
||||||
response = self.scan(
|
response = self.scan(
|
||||||
@ -423,7 +423,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.data['supplierpart']['pk'], 1)
|
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('success', response.data)
|
||||||
self.assertIn('barcode_data', response.data)
|
self.assertIn('barcode_data', response.data)
|
||||||
|
@ -27,8 +27,10 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin):
|
|||||||
"""Core notification methods for InvenTree."""
|
"""Core notification methods for InvenTree."""
|
||||||
|
|
||||||
NAME = "CoreNotificationsPlugin"
|
NAME = "CoreNotificationsPlugin"
|
||||||
|
TITLE = _("InvenTree Notifications")
|
||||||
AUTHOR = _('InvenTree contributors')
|
AUTHOR = _('InvenTree contributors')
|
||||||
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
SETTINGS = {
|
SETTINGS = {
|
||||||
'ENABLE_NOTIFICATION_EMAILS': {
|
'ENABLE_NOTIFICATION_EMAILS': {
|
||||||
|
@ -158,16 +158,20 @@ class PluginConfig(models.Model):
|
|||||||
@admin.display(boolean=True, description=_('Sample plugin'))
|
@admin.display(boolean=True, description=_('Sample plugin'))
|
||||||
def is_sample(self) -> bool:
|
def is_sample(self) -> bool:
|
||||||
"""Is this plugin a sample app?"""
|
"""Is this plugin a sample app?"""
|
||||||
# Loaded and active plugin
|
|
||||||
if isinstance(self.plugin, InvenTreePlugin):
|
|
||||||
return self.plugin.check_is_sample()
|
|
||||||
|
|
||||||
# If no plugin_class is available it can not be a sample
|
|
||||||
if not self.plugin:
|
if not self.plugin:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Not loaded plugin
|
return self.plugin.check_is_sample()
|
||||||
return self.plugin.check_is_sample() # pragma: no cover
|
|
||||||
|
@admin.display(boolean=True, description=_('Builtin Plugin'))
|
||||||
|
def is_builtin(self) -> bool:
|
||||||
|
"""Return True if this is a 'builtin' plugin"""
|
||||||
|
|
||||||
|
if not self.plugin:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.plugin.check_is_builtin()
|
||||||
|
|
||||||
|
|
||||||
class PluginSetting(common.models.BaseInvenTreeSetting):
|
class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||||
|
@ -106,10 +106,15 @@ class MetaBase:
|
|||||||
|
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""Return True if this plugin is currently active."""
|
"""Return True if this plugin is currently active."""
|
||||||
cfg = self.plugin_config()
|
|
||||||
|
|
||||||
if cfg:
|
# Builtin plugins are always considered "active"
|
||||||
return cfg.active
|
if self.is_builtin:
|
||||||
|
return True
|
||||||
|
|
||||||
|
config = self.plugin_config()
|
||||||
|
|
||||||
|
if config:
|
||||||
|
return config.active
|
||||||
else:
|
else:
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
|
|
||||||
@ -300,6 +305,16 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
|
|||||||
"""Is this plugin part of the samples?"""
|
"""Is this plugin part of the samples?"""
|
||||||
return self.check_is_sample()
|
return self.check_is_sample()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_is_builtin(cls) -> bool:
|
||||||
|
"""Determine if a particular plugin class is a 'builtin' plugin"""
|
||||||
|
return str(cls.check_package_path()).startswith('plugin/builtin')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_builtin(self) -> bool:
|
||||||
|
"""Is this plugin is builtin"""
|
||||||
|
return self.check_is_builtin()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_package_path(cls):
|
def check_package_path(cls):
|
||||||
"""Path to the plugin."""
|
"""Path to the plugin."""
|
||||||
|
@ -108,9 +108,6 @@ class PluginsRegistry:
|
|||||||
Args:
|
Args:
|
||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
"""
|
"""
|
||||||
if not settings.PLUGINS_ENABLED:
|
|
||||||
# Plugins not enabled, do nothing
|
|
||||||
return # pragma: no cover
|
|
||||||
|
|
||||||
logger.info('Start loading plugins')
|
logger.info('Start loading plugins')
|
||||||
|
|
||||||
@ -167,9 +164,6 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
def unload_plugins(self):
|
def unload_plugins(self):
|
||||||
"""Unload and deactivate all IntegrationPlugins."""
|
"""Unload and deactivate all IntegrationPlugins."""
|
||||||
if not settings.PLUGINS_ENABLED:
|
|
||||||
# Plugins not enabled, do nothing
|
|
||||||
return # pragma: no cover
|
|
||||||
|
|
||||||
logger.info('Start unloading plugins')
|
logger.info('Start unloading plugins')
|
||||||
|
|
||||||
@ -187,6 +181,7 @@ class PluginsRegistry:
|
|||||||
# remove maintenance
|
# remove maintenance
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
set_maintenance_mode(False) # pragma: no cover
|
set_maintenance_mode(False) # pragma: no cover
|
||||||
|
|
||||||
logger.info('Finished unloading plugins')
|
logger.info('Finished unloading plugins')
|
||||||
|
|
||||||
def reload_plugins(self, full_reload: bool = False):
|
def reload_plugins(self, full_reload: bool = False):
|
||||||
@ -210,8 +205,12 @@ class PluginsRegistry:
|
|||||||
def plugin_dirs(self):
|
def plugin_dirs(self):
|
||||||
"""Construct a list of directories from where plugins can be loaded"""
|
"""Construct a list of directories from where plugins can be loaded"""
|
||||||
|
|
||||||
|
# Builtin plugins are *always* loaded
|
||||||
dirs = ['plugin.builtin', ]
|
dirs = ['plugin.builtin', ]
|
||||||
|
|
||||||
|
if settings.PLUGINS_ENABLED:
|
||||||
|
# Any 'external' plugins are only loaded if PLUGINS_ENABLED is set to True
|
||||||
|
|
||||||
if settings.TESTING or settings.DEBUG:
|
if settings.TESTING or settings.DEBUG:
|
||||||
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
|
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
|
||||||
dirs.append('plugin.samples')
|
dirs.append('plugin.samples')
|
||||||
@ -263,9 +262,6 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
def collect_plugins(self):
|
def collect_plugins(self):
|
||||||
"""Collect plugins from all possible ways of loading. Returned as list."""
|
"""Collect plugins from all possible ways of loading. Returned as list."""
|
||||||
if not settings.PLUGINS_ENABLED:
|
|
||||||
# Plugins not enabled, do nothing
|
|
||||||
return # pragma: no cover
|
|
||||||
|
|
||||||
collected_plugins = []
|
collected_plugins = []
|
||||||
|
|
||||||
@ -293,6 +289,9 @@ class PluginsRegistry:
|
|||||||
if modules:
|
if modules:
|
||||||
[collected_plugins.append(item) for item in modules]
|
[collected_plugins.append(item) for item in modules]
|
||||||
|
|
||||||
|
# From this point any plugins are considered "external" and only loaded if plugins are explicitly enabled
|
||||||
|
if settings.PLUGINS_ENABLED:
|
||||||
|
|
||||||
# Check if not running in testing mode and apps should be loaded from hooks
|
# Check if not running in testing mode and apps should be loaded from hooks
|
||||||
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
|
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
|
||||||
# Collect plugins from setup entry points
|
# Collect plugins from setup entry points
|
||||||
@ -335,7 +334,7 @@ class PluginsRegistry:
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region registry functions
|
# region registry functions
|
||||||
def with_mixin(self, mixin: str, active=None):
|
def with_mixin(self, mixin: str, active=None, builtin=None):
|
||||||
"""Returns reference to all plugins that have a specified mixin enabled."""
|
"""Returns reference to all plugins that have a specified mixin enabled."""
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@ -343,10 +342,13 @@ class PluginsRegistry:
|
|||||||
if plugin.mixin_enabled(mixin):
|
if plugin.mixin_enabled(mixin):
|
||||||
|
|
||||||
if active is not None:
|
if active is not None:
|
||||||
# Filter by 'enabled' status
|
# Filter by 'active' status of plugin
|
||||||
config = plugin.plugin_config()
|
if active != plugin.is_active():
|
||||||
|
continue
|
||||||
|
|
||||||
if config.active != active:
|
if builtin is not None:
|
||||||
|
# Filter by 'builtin' status of plugin
|
||||||
|
if builtin != plugin.is_builtin:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result.append(plugin)
|
result.append(plugin)
|
||||||
@ -403,8 +405,14 @@ class PluginsRegistry:
|
|||||||
# Append reference to plugin
|
# Append reference to plugin
|
||||||
plg.db = plg_db
|
plg.db = plg_db
|
||||||
|
|
||||||
# Always activate if testing
|
# Check if this is a 'builtin' plugin
|
||||||
if settings.PLUGIN_TESTING or (plg_db and plg_db.active):
|
builtin = plg.check_is_builtin()
|
||||||
|
|
||||||
|
# Determine if this plugin should be loaded:
|
||||||
|
# - If PLUGIN_TESTING is enabled
|
||||||
|
# - If this is a 'builtin' plugin
|
||||||
|
# - If this plugin has been explicitly enabled by the user
|
||||||
|
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
|
||||||
# Check if the plugin was blocked -> threw an error; option1: package, option2: file-based
|
# Check if the plugin was blocked -> threw an error; option1: package, option2: file-based
|
||||||
if disabled and ((plg.__name__ == disabled) or (plg.__module__ == disabled)):
|
if disabled and ((plg.__name__ == disabled) or (plg.__module__ == disabled)):
|
||||||
safe_reference(plugin=plg, key=plg_key, active=False)
|
safe_reference(plugin=plg, key=plg_key, active=False)
|
||||||
@ -498,10 +506,9 @@ class PluginsRegistry:
|
|||||||
for _key, plugin in plugins:
|
for _key, plugin in plugins:
|
||||||
|
|
||||||
if plugin.mixin_enabled('schedule'):
|
if plugin.mixin_enabled('schedule'):
|
||||||
config = plugin.plugin_config()
|
|
||||||
|
|
||||||
|
if plugin.is_active():
|
||||||
# Only active tasks for plugins which are enabled
|
# Only active tasks for plugins which are enabled
|
||||||
if config and config.active:
|
|
||||||
plugin.register_tasks()
|
plugin.register_tasks()
|
||||||
task_keys += plugin.get_task_names()
|
task_keys += plugin.get_task_names()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Unit tests for action plugins."""
|
"""Unit tests for action plugins."""
|
||||||
|
|
||||||
from InvenTree.helpers import InvenTreeTestCase
|
from InvenTree.helpers import InvenTreeTestCase
|
||||||
from plugin.builtin.action.simpleactionplugin import SimpleActionPlugin
|
from plugin.samples.integration.simpleactionplugin import SimpleActionPlugin
|
||||||
|
|
||||||
|
|
||||||
class SimpleActionPluginTests(InvenTreeTestCase):
|
class SimpleActionPluginTests(InvenTreeTestCase):
|
@ -28,25 +28,38 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
url = reverse('api-plugin-install')
|
url = reverse('api-plugin-install')
|
||||||
|
|
||||||
# valid - Pypi
|
# valid - Pypi
|
||||||
data = self.post(url, {
|
data = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
'confirm': True,
|
'confirm': True,
|
||||||
'packagename': self.PKG_NAME
|
'packagename': self.PKG_NAME
|
||||||
}, expected_code=201).data
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).data
|
||||||
|
|
||||||
self.assertEqual(data['success'], True)
|
self.assertEqual(data['success'], True)
|
||||||
|
|
||||||
# valid - github url
|
# valid - github url
|
||||||
data = self.post(url, {
|
data = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
'confirm': True,
|
'confirm': True,
|
||||||
'url': self.PKG_URL
|
'url': self.PKG_URL
|
||||||
}, expected_code=201).data
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).data
|
||||||
self.assertEqual(data['success'], True)
|
self.assertEqual(data['success'], True)
|
||||||
|
|
||||||
# valid - github url and packagename
|
# valid - github url and packagename
|
||||||
data = self.post(url, {
|
data = self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
'confirm': True,
|
'confirm': True,
|
||||||
'url': self.PKG_URL,
|
'url': self.PKG_URL,
|
||||||
'packagename': 'minimal',
|
'packagename': 'minimal',
|
||||||
}, expected_code=201).data
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).data
|
||||||
self.assertEqual(data['success'], True)
|
self.assertEqual(data['success'], True)
|
||||||
|
|
||||||
# invalid tries
|
# invalid tries
|
||||||
@ -57,17 +70,20 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
data = self.post(url, {
|
data = self.post(url, {
|
||||||
'confirm': True,
|
'confirm': True,
|
||||||
}, expected_code=400).data
|
}, expected_code=400).data
|
||||||
|
|
||||||
self.assertEqual(data['url'][0].title().upper(), self.MSG_NO_PKG.upper())
|
self.assertEqual(data['url'][0].title().upper(), self.MSG_NO_PKG.upper())
|
||||||
self.assertEqual(data['packagename'][0].title().upper(), self.MSG_NO_PKG.upper())
|
self.assertEqual(data['packagename'][0].title().upper(), self.MSG_NO_PKG.upper())
|
||||||
|
|
||||||
# not confirmed
|
# not confirmed
|
||||||
self.post(url, {
|
self.post(url, {
|
||||||
'packagename': self.PKG_NAME
|
'packagename': self.PKG_NAME
|
||||||
}, expected_code=400).data
|
}, expected_code=400)
|
||||||
|
|
||||||
data = self.post(url, {
|
data = self.post(url, {
|
||||||
'packagename': self.PKG_NAME,
|
'packagename': self.PKG_NAME,
|
||||||
'confirm': False,
|
'confirm': False,
|
||||||
}, expected_code=400).data
|
}, expected_code=400).data
|
||||||
|
|
||||||
self.assertEqual(data['confirm'][0].title().upper(), 'Installation not confirmed'.upper())
|
self.assertEqual(data['confirm'][0].title().upper(), 'Installation not confirmed'.upper())
|
||||||
|
|
||||||
def test_admin_action(self):
|
def test_admin_action(self):
|
||||||
|
@ -305,6 +305,7 @@ class TestReportTest(ReportTest):
|
|||||||
InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None)
|
InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None)
|
||||||
|
|
||||||
response = self.get(url, {'item': item.pk}, expected_code=200)
|
response = self.get(url, {'item': item.pk}, expected_code=200)
|
||||||
|
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
self.assertEqual(headers['Content-Type'], 'application/pdf')
|
self.assertEqual(headers['Content-Type'], 'application/pdf')
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% plugins_enabled as plug %}
|
||||||
|
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<div class='d-flex flex-wrap'>
|
<div class='d-flex flex-wrap'>
|
||||||
<h4>{% trans "Plugins" %}</h4>
|
<h4>{% trans "Plugins" %}</h4>
|
||||||
@ -38,78 +40,46 @@
|
|||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% url 'admin:plugin_pluginconfig_changelist' as url %}
|
{% url 'admin:plugin_pluginconfig_changelist' as url %}
|
||||||
{% include "admin_button.html" with url=url %}
|
{% include "admin_button.html" with url=url %}
|
||||||
|
{% if plug %}
|
||||||
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}"><span class='fas fa-plus-circle'></span> {% trans "Install Plugin" %}</button>
|
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}"><span class='fas fa-plus-circle'></span> {% trans "Install Plugin" %}</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if not plug %}
|
||||||
|
<div class='alert alert-warning alert-block'>
|
||||||
|
{% trans "External plugins are not enabled for this InvenTree installation" %}<br>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class='table-responsive'>
|
<div class='table-responsive'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Admin" %}</th>
|
|
||||||
<th>{% trans "Name" %}</th>
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Key" %}</th>
|
||||||
<th>{% trans "Author" %}</th>
|
<th>{% trans "Author" %}</th>
|
||||||
<th>{% trans "Date" %}</th>
|
<th>{% trans "Date" %}</th>
|
||||||
<th>{% trans "Version" %}</th>
|
<th>{% trans "Version" %}</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% plugin_list as pl_list %}
|
{% plugin_list as pl_list %}
|
||||||
|
{% if pl_list %}
|
||||||
|
<tr><td colspan="6"><h6>{% trans 'Active plugins' %}</h6></td></tr>
|
||||||
{% for plugin_key, plugin in pl_list.items %}
|
{% for plugin_key, plugin in pl_list.items %}
|
||||||
{% mixin_enabled plugin 'urls' as urls %}
|
{% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
|
||||||
{% mixin_enabled plugin 'settings' as settings %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if user.is_staff and perms.plugin.change_pluginconfig %}
|
|
||||||
{% url 'admin:plugin_pluginconfig_change' plugin.pk as url %}
|
|
||||||
{% include "admin_button.html" with url=url %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ plugin.human_name }}<span class="text-muted"> - {{plugin_key}}</span>
|
|
||||||
{% define plugin.registered_mixins as mixin_list %}
|
|
||||||
|
|
||||||
{% if plugin.is_sample %}
|
|
||||||
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
|
||||||
<span class='badge bg-info rounded-pill badge-right'>{% trans "Sample" %}</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if mixin_list %}
|
|
||||||
{% for mixin in mixin_list %}
|
|
||||||
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
|
||||||
<span class='badge bg-dark badge-right rounded-pill'>{{ mixin.human_name }}</span>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if plugin.website %}
|
|
||||||
<a href="{{ plugin.website }}"><span class="fas fa-globe"></span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ plugin.author }}</td>
|
|
||||||
<td>{% render_date plugin.pub_date %}</td>
|
|
||||||
<td>{% if plugin.version %}{{ plugin.version }}{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% inactive_plugin_list as in_pl_list %}
|
{% inactive_plugin_list as in_pl_list %}
|
||||||
{% if in_pl_list %}
|
{% if in_pl_list %}
|
||||||
<tr><td colspan="5"></td></tr>
|
<tr><td colspan="6"><h6>{% trans 'Inactive plugins' %}</h6></td></tr>
|
||||||
<tr><td colspan="5"><h6>{% trans 'Inactive plugins' %}</h6></td></tr>
|
|
||||||
{% for plugin_key, plugin in in_pl_list.items %}
|
{% for plugin_key, plugin in in_pl_list.items %}
|
||||||
<tr>
|
{% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
|
||||||
<td>
|
|
||||||
{% if user.is_staff and perms.plugin.change_pluginconfig %}
|
|
||||||
{% url 'admin:plugin_pluginconfig_change' plugin.pk as url %}
|
|
||||||
{% include "admin_button.html" with url=url %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{plugin.name}}<span class="text-muted"> - {{plugin.key}}</span></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
75
InvenTree/templates/InvenTree/settings/plugin_details.html
Normal file
75
InvenTree/templates/InvenTree/settings/plugin_details.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{% load inventree_extras %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if plugin.is_active %}
|
||||||
|
<span class='fas fa-check-circle icon-green'></span>
|
||||||
|
{% else %}
|
||||||
|
<span class='fas fa-times-circle icon-red'></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if plugin.human_name %}
|
||||||
|
{{ plugin.human_name }}
|
||||||
|
{% elif plugin.title %}
|
||||||
|
{{ plugin.title }}
|
||||||
|
{% elif plugin.name %}
|
||||||
|
{{ plugin.name }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% define plugin.registered_mixins as mixin_list %}
|
||||||
|
|
||||||
|
{% if mixin_list %}
|
||||||
|
{% for mixin in mixin_list %}
|
||||||
|
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
||||||
|
<span class='badge bg-dark badge-right rounded-pill'>{{ mixin.human_name }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if plugin.is_builtin %}
|
||||||
|
<a class='sidebar-selector' id='select-plugin-{{ plugin_key }}' data-bs-parent='#sidebar'>
|
||||||
|
<span class='badge bg-success rounded-pill badge-right'>{% trans "Builtin" %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if plugin.is_sample %}
|
||||||
|
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
||||||
|
<span class='badge bg-info rounded-pill badge-right'>{% trans "Sample" %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if plugin.website %}
|
||||||
|
<a href="{{ plugin.website }}"><span class="fas fa-globe"></span></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ plugin_key }}</td>
|
||||||
|
{% trans "Unvailable" as no_info %}
|
||||||
|
<td>
|
||||||
|
{% if plugin.author %}
|
||||||
|
{{ plugin.author }}
|
||||||
|
{% else %}
|
||||||
|
<em>{{ no_info }}</em>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if plugin.pub_date %}
|
||||||
|
{% render_date plugin.pub_date %}
|
||||||
|
{% else %}
|
||||||
|
<em>{{ no_info }}</em>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if plugin.version %}
|
||||||
|
{{ plugin.version }}
|
||||||
|
{% else %}
|
||||||
|
<em>{{ no_info }}</em>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_staff and perms.plugin.change_pluginconfig %}
|
||||||
|
{% url 'admin:plugin_pluginconfig_change' plugin.pk as url %}
|
||||||
|
{% include "admin_button.html" with url=url %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
@ -23,16 +23,16 @@
|
|||||||
<td>{% trans "Name" %}</td>
|
<td>{% trans "Name" %}</td>
|
||||||
<td>{{ plugin.human_name }}{% include "clip.html" %}</td>
|
<td>{{ plugin.human_name }}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-user'></span></span></td>
|
|
||||||
<td>{% trans "Author" %}</td>
|
|
||||||
<td>{{ plugin.author }}{% include "clip.html" %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{% trans "Description" %}</td>
|
<td>{% trans "Description" %}</td>
|
||||||
<td>{{ plugin.description }}{% include "clip.html" %}</td>
|
<td>{{ plugin.description }}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-user'></span></span></td>
|
||||||
|
<td>{% trans "Author" %}</td>
|
||||||
|
<td>{{ plugin.author }}{% include "clip.html" %}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Date" %}</td>
|
<td>{% trans "Date" %}</td>
|
||||||
@ -94,7 +94,14 @@
|
|||||||
<td>{% trans "Installation path" %}</td>
|
<td>{% trans "Installation path" %}</td>
|
||||||
<td>{{ plugin.package_path }}</td>
|
<td>{{ plugin.package_path }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if plugin.is_package == False %}
|
{% if plugin.is_package %}
|
||||||
|
{% elif plugin.is_builtin %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-check-circle icon-green'></span></td>
|
||||||
|
<td>{% trans "Builtin" %}</td>
|
||||||
|
<td>{% trans "This is a builtin plugin which cannot be disabled" %}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-user'></span></td>
|
<td><span class='fas fa-user'></span></td>
|
||||||
<td>{% trans "Commit Author" %}</td><td>{{ plugin.package.author }} - {{ plugin.package.mail }}{% include "clip.html" %}</td>
|
<td>{% trans "Commit Author" %}</td><td>{{ plugin.package.author }} - {{ plugin.package.mail }}{% include "clip.html" %}</td>
|
||||||
|
@ -42,8 +42,6 @@
|
|||||||
{% include "InvenTree/settings/po.html" %}
|
{% include "InvenTree/settings/po.html" %}
|
||||||
{% include "InvenTree/settings/so.html" %}
|
{% include "InvenTree/settings/so.html" %}
|
||||||
|
|
||||||
{% plugins_enabled as plug %}
|
|
||||||
{% if plug %}
|
|
||||||
{% include "InvenTree/settings/plugin.html" %}
|
{% include "InvenTree/settings/plugin.html" %}
|
||||||
{% plugin_list as pl_list %}
|
{% plugin_list as pl_list %}
|
||||||
{% for plugin_key, plugin in pl_list.items %}
|
{% for plugin_key, plugin in pl_list.items %}
|
||||||
@ -51,7 +49,6 @@
|
|||||||
{% include "InvenTree/settings/plugin_settings.html" %}
|
{% include "InvenTree/settings/plugin_settings.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -51,8 +51,7 @@
|
|||||||
{% trans "Sales Orders" as text %}
|
{% trans "Sales Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %}
|
{% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %}
|
||||||
|
|
||||||
{% plugins_enabled as plug %}
|
|
||||||
{% if plug %}
|
|
||||||
{% trans "Plugin Settings" as text %}
|
{% trans "Plugin Settings" as text %}
|
||||||
{% include "sidebar_header.html" with text=text %}
|
{% include "sidebar_header.html" with text=text %}
|
||||||
{% trans "Plugins" as text %}
|
{% trans "Plugins" as text %}
|
||||||
@ -64,6 +63,5 @@
|
|||||||
{% include "sidebar_item.html" with label='plugin-'|add:plugin_key text=plugin.human_name %}
|
{% include "sidebar_item.html" with label='plugin-'|add:plugin_key text=plugin.human_name %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<button id='admin-button' title='{% trans "View in administration panel" %}' type='button' class='btn btn-primary admin-button' url='{{ url }}'>
|
|
||||||
|
{% inventree_customize 'hide_admin_link' as hidden %}
|
||||||
|
|
||||||
|
{% if not hidden and user.is_staff %}
|
||||||
|
<a href='{{ url }}'>
|
||||||
|
<button id='admin-button' href='{{ url }}' title='{% trans "View in administration panel" %}' type='button' class='btn btn-primary admin-button'>
|
||||||
<span class='fas fa-user-shield'></span>
|
<span class='fas fa-user-shield'></span>
|
||||||
</button>
|
</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user