mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[Feature] Mandatory Plugins (#9339)
* Define which builtin plugins are always-active * Adds 'mandatory' property to PluginConfig * Update API / frontend * Fix form method * Tweaks * Bump API version * Tweak unit tests
This commit is contained in:
parent
ae1ec31ca9
commit
bdc5f9e84e
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 326
|
INVENTREE_API_VERSION = 327
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v327 - 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339
|
||||||
|
- Adds "is_mandatory" field to the Plugin API
|
||||||
|
- Adds ability to filter by "mandatory" status in the Plugin API
|
||||||
|
|
||||||
v326 - 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
|
v326 - 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
|
||||||
- Overhaul the data-export API functionality
|
- Overhaul the data-export API functionality
|
||||||
- Allow customization of data exporting via plugins
|
- Allow customization of data exporting via plugins
|
||||||
|
@ -18,6 +18,7 @@ import plugin.serializers as PluginSerializers
|
|||||||
from common.api import GlobalSettingsPermissions
|
from common.api import GlobalSettingsPermissions
|
||||||
from InvenTree.api import MetadataView
|
from InvenTree.api import MetadataView
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||||
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.mixins import (
|
from InvenTree.mixins import (
|
||||||
CreateAPI,
|
CreateAPI,
|
||||||
ListAPI,
|
ListAPI,
|
||||||
@ -27,13 +28,13 @@ from InvenTree.mixins import (
|
|||||||
UpdateAPI,
|
UpdateAPI,
|
||||||
)
|
)
|
||||||
from InvenTree.permissions import IsSuperuser, IsSuperuserOrReadOnly
|
from InvenTree.permissions import IsSuperuser, IsSuperuserOrReadOnly
|
||||||
from plugin import registry
|
|
||||||
from plugin.base.action.api import ActionPluginView
|
from plugin.base.action.api import ActionPluginView
|
||||||
from plugin.base.barcodes.api import barcode_api_urls
|
from plugin.base.barcodes.api import barcode_api_urls
|
||||||
from plugin.base.locate.api import LocatePluginView
|
from plugin.base.locate.api import LocatePluginView
|
||||||
from plugin.base.ui.api import ui_plugins_api_urls
|
from plugin.base.ui.api import ui_plugins_api_urls
|
||||||
from plugin.models import PluginConfig, PluginSetting
|
from plugin.models import PluginConfig, PluginSetting
|
||||||
from plugin.plugin import InvenTreePlugin
|
from plugin.plugin import InvenTreePlugin
|
||||||
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
|
||||||
class PluginFilter(rest_filters.FilterSet):
|
class PluginFilter(rest_filters.FilterSet):
|
||||||
@ -75,7 +76,7 @@ class PluginFilter(rest_filters.FilterSet):
|
|||||||
return queryset.filter(pk__in=matches)
|
return queryset.filter(pk__in=matches)
|
||||||
|
|
||||||
builtin = rest_filters.BooleanFilter(
|
builtin = rest_filters.BooleanFilter(
|
||||||
field_name='builtin', label='Builtin', method='filter_builtin'
|
field_name='builtin', label=_('Builtin'), method='filter_builtin'
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_builtin(self, queryset, name, value):
|
def filter_builtin(self, queryset, name, value):
|
||||||
@ -88,8 +89,19 @@ class PluginFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
return queryset.filter(pk__in=matches)
|
return queryset.filter(pk__in=matches)
|
||||||
|
|
||||||
|
mandatory = rest_filters.BooleanFilter(
|
||||||
|
field_name='mandatory', label=_('Mandatory'), method='filter_mandatory'
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_mandatory(self, queryset, name, value):
|
||||||
|
"""Filter by 'mandatory' flag."""
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.filter(key__in=registry.MANDATORY_PLUGINS)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(key__in=registry.MANDATORY_PLUGINS)
|
||||||
|
|
||||||
sample = rest_filters.BooleanFilter(
|
sample = rest_filters.BooleanFilter(
|
||||||
field_name='sample', label='Sample', method='filter_sample'
|
field_name='sample', label=_('Sample'), method='filter_sample'
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_sample(self, queryset, name, value):
|
def filter_sample(self, queryset, name, value):
|
||||||
@ -103,7 +115,7 @@ class PluginFilter(rest_filters.FilterSet):
|
|||||||
return queryset.filter(pk__in=matches)
|
return queryset.filter(pk__in=matches)
|
||||||
|
|
||||||
installed = rest_filters.BooleanFilter(
|
installed = rest_filters.BooleanFilter(
|
||||||
field_name='installed', label='Installed', method='filter_installed'
|
field_name='installed', label=_('Installed'), method='filter_installed'
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_installed(self, queryset, name, value):
|
def filter_installed(self, queryset, name, value):
|
||||||
@ -135,8 +147,6 @@ class PluginList(ListAPI):
|
|||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
filterset_fields = ['active']
|
|
||||||
|
|
||||||
ordering_fields = ['key', 'name', 'active']
|
ordering_fields = ['key', 'name', 'active']
|
||||||
|
|
||||||
ordering = ['-active', 'name', 'key']
|
ordering = ['-active', 'name', 'key']
|
||||||
|
@ -15,6 +15,11 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
SCAN_URL = reverse('api-barcode-scan')
|
SCAN_URL = reverse('api-barcode-scan')
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Ensure the digikey plugin is enabled."""
|
||||||
|
super().setUp()
|
||||||
|
registry.set_plugin_state('digikeyplugin', True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Create supplier parts for barcodes."""
|
"""Create supplier parts for barcodes."""
|
||||||
|
@ -13,8 +13,9 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import common.models
|
import common.models
|
||||||
import InvenTree.models
|
import InvenTree.models
|
||||||
import plugin.staticfiles
|
import plugin.staticfiles
|
||||||
from plugin import InvenTreePlugin, registry
|
from plugin import InvenTreePlugin
|
||||||
from plugin.events import PluginEvents, trigger_event
|
from plugin.events import PluginEvents, trigger_event
|
||||||
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
|
||||||
class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||||
@ -146,8 +147,8 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
|||||||
|
|
||||||
super().save(force_insert, force_update, *args, **kwargs)
|
super().save(force_insert, force_update, *args, **kwargs)
|
||||||
|
|
||||||
if self.is_builtin():
|
if self.is_mandatory():
|
||||||
# Force active if builtin
|
# Force active if mandatory plugin
|
||||||
self.active = True
|
self.active = True
|
||||||
|
|
||||||
if not no_reload and self.active != self.__org_active:
|
if not no_reload and self.active != self.__org_active:
|
||||||
@ -180,6 +181,11 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
|||||||
|
|
||||||
return self.plugin.check_is_builtin()
|
return self.plugin.check_is_builtin()
|
||||||
|
|
||||||
|
@admin.display(boolean=True, description=_('Mandatory Plugin'))
|
||||||
|
def is_mandatory(self) -> bool:
|
||||||
|
"""Return True if this plugin is mandatory."""
|
||||||
|
return self.key in registry.MANDATORY_PLUGINS
|
||||||
|
|
||||||
@admin.display(boolean=True, description=_('Package Plugin'))
|
@admin.display(boolean=True, description=_('Package Plugin'))
|
||||||
def is_package(self) -> bool:
|
def is_package(self) -> bool:
|
||||||
"""Return True if this is a 'package' plugin."""
|
"""Return True if this is a 'package' plugin."""
|
||||||
|
@ -53,6 +53,21 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
DEFAULT_MIXIN_ORDER = [SettingsMixin, ScheduleMixin, AppMixin, UrlsMixin]
|
DEFAULT_MIXIN_ORDER = [SettingsMixin, ScheduleMixin, AppMixin, UrlsMixin]
|
||||||
|
|
||||||
|
# This list of plugins are *always* enabled, and are loaded by default
|
||||||
|
# This is because they provide core functionality to the InvenTree system
|
||||||
|
# Other 'builtin' plugins are automatically loaded, but can be disabled by the user
|
||||||
|
MANDATORY_PLUGINS = [
|
||||||
|
'inventreebarcode',
|
||||||
|
'bom-exporter',
|
||||||
|
'inventree-exporter',
|
||||||
|
'inventreecorenotificationsplugin',
|
||||||
|
'inventreecurrencyexchange',
|
||||||
|
'inventreecorenotificationsplugin',
|
||||||
|
'inventreelabel',
|
||||||
|
'inventreelabelmachine',
|
||||||
|
'inventreelabelsheet',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize registry.
|
"""Initialize registry.
|
||||||
|
|
||||||
@ -518,10 +533,11 @@ class PluginsRegistry:
|
|||||||
if getattr(plugin, 'is_package', False):
|
if getattr(plugin, 'is_package', False):
|
||||||
package_name = getattr(plugin, 'package_name', None)
|
package_name = getattr(plugin, 'package_name', None)
|
||||||
|
|
||||||
# Auto-enable builtin plugins
|
# Auto-enable default builtin plugins
|
||||||
if builtin and plg_db and not plg_db.active:
|
if builtin and plg_db and plg_db.is_mandatory():
|
||||||
plg_db.active = True
|
if not plg_db.active:
|
||||||
plg_db.save()
|
plg_db.active = True
|
||||||
|
plg_db.save()
|
||||||
|
|
||||||
# Save the package_name attribute to the plugin
|
# Save the package_name attribute to the plugin
|
||||||
if plg_db.package_name != package_name:
|
if plg_db.package_name != package_name:
|
||||||
|
@ -59,12 +59,14 @@ class PluginConfigSerializer(serializers.ModelSerializer):
|
|||||||
'is_sample',
|
'is_sample',
|
||||||
'is_installed',
|
'is_installed',
|
||||||
'is_package',
|
'is_package',
|
||||||
|
'is_mandatory',
|
||||||
]
|
]
|
||||||
|
|
||||||
read_only_fields = ['key', 'is_builtin', 'is_sample', 'is_installed']
|
read_only_fields = ['key', 'is_builtin', 'is_sample', 'is_installed']
|
||||||
|
|
||||||
meta = serializers.DictField(read_only=True)
|
meta = serializers.DictField(read_only=True)
|
||||||
mixins = serializers.DictField(read_only=True)
|
mixins = serializers.DictField(read_only=True)
|
||||||
|
is_mandatory = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class PluginAdminDetailSerializer(serializers.ModelSerializer):
|
class PluginAdminDetailSerializer(serializers.ModelSerializer):
|
||||||
|
@ -14,7 +14,6 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
|
||||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||||
import PluginDrawer from '../../components/plugins/PluginDrawer';
|
import PluginDrawer from '../../components/plugins/PluginDrawer';
|
||||||
import type { PluginInterface } from '../../components/plugins/PluginInterface';
|
import type { PluginInterface } from '../../components/plugins/PluginInterface';
|
||||||
@ -29,6 +28,7 @@ import { useTable } from '../../hooks/UseTable';
|
|||||||
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import type { TableColumn } from '../Column';
|
import type { TableColumn } from '../Column';
|
||||||
|
import { BooleanColumn } from '../ColumnRenderers';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import type { RowAction } from '../RowActions';
|
import type { RowAction } from '../RowActions';
|
||||||
|
|
||||||
@ -89,12 +89,21 @@ export default function PluginListTable() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
BooleanColumn({
|
||||||
accessor: 'active',
|
accessor: 'active',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Active`,
|
title: t`Active`
|
||||||
render: (record: any) => <YesNoButton value={record.active} />
|
}),
|
||||||
},
|
BooleanColumn({
|
||||||
|
accessor: 'is_builtin',
|
||||||
|
sortable: false,
|
||||||
|
title: t`Builtin`
|
||||||
|
}),
|
||||||
|
BooleanColumn({
|
||||||
|
accessor: 'is_mandatory',
|
||||||
|
sortable: false,
|
||||||
|
title: t`Mandatory`
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'meta.description',
|
accessor: 'meta.description',
|
||||||
title: t`Description`,
|
title: t`Description`,
|
||||||
@ -164,7 +173,7 @@ export default function PluginListTable() {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
hidden: record.is_builtin != false || record.active != true,
|
hidden: record.is_mandatory != false || record.active != true,
|
||||||
title: t`Deactivate`,
|
title: t`Deactivate`,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <IconCircleX />,
|
icon: <IconCircleX />,
|
||||||
@ -176,7 +185,7 @@ export default function PluginListTable() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden:
|
hidden:
|
||||||
record.is_builtin != false ||
|
record.is_mandatory != false ||
|
||||||
!record.is_installed ||
|
!record.is_installed ||
|
||||||
record.active != false,
|
record.active != false,
|
||||||
title: t`Activate`,
|
title: t`Activate`,
|
||||||
@ -207,6 +216,7 @@ export default function PluginListTable() {
|
|||||||
!user.isSuperuser() ||
|
!user.isSuperuser() ||
|
||||||
record.active ||
|
record.active ||
|
||||||
record.is_builtin ||
|
record.is_builtin ||
|
||||||
|
record.is_mandatory ||
|
||||||
record.is_sample ||
|
record.is_sample ||
|
||||||
!record.is_installed ||
|
!record.is_installed ||
|
||||||
!record.is_package,
|
!record.is_package,
|
||||||
@ -225,6 +235,7 @@ export default function PluginListTable() {
|
|||||||
hidden:
|
hidden:
|
||||||
record.active ||
|
record.active ||
|
||||||
record.is_builtin ||
|
record.is_builtin ||
|
||||||
|
record.is_mandatory ||
|
||||||
record.is_sample ||
|
record.is_sample ||
|
||||||
record.is_installed ||
|
record.is_installed ||
|
||||||
!user.isSuperuser(),
|
!user.isSuperuser(),
|
||||||
@ -405,6 +416,11 @@ export default function PluginListTable() {
|
|||||||
label: t`Builtin`,
|
label: t`Builtin`,
|
||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'mandatory',
|
||||||
|
label: t`Mandatory`,
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'sample',
|
name: 'sample',
|
||||||
label: t`Sample`,
|
label: t`Sample`,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user