diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py
index 4947df66e1..5568e5595b 100644
--- a/InvenTree/plugin/api.py
+++ b/InvenTree/plugin/api.py
@@ -70,6 +70,8 @@ class PluginList(ListAPI):
]
ordering = [
+ '-active',
+ 'name',
'key',
]
@@ -120,10 +122,17 @@ class PluginInstall(CreateAPI):
class PluginActivate(UpdateAPI):
- """Endpoint for activating a plugin."""
+ """Endpoint for activating a plugin.
+
+ - PATCH: Activate a plugin
+
+ Pass a boolean value for the 'active' field.
+ If not provided, it is assumed to be True,
+ and the plugin will be activated.
+ """
queryset = PluginConfig.objects.all()
- serializer_class = PluginSerializers.PluginConfigEmptySerializer
+ serializer_class = PluginSerializers.PluginActivateSerializer
permission_classes = [IsSuperuser, ]
def get_object(self):
@@ -134,9 +143,8 @@ class PluginActivate(UpdateAPI):
def perform_update(self, serializer):
"""Activate the plugin."""
- instance = serializer.instance
- instance.active = True
- instance.save()
+
+ serializer.save()
class PluginSettingList(ListAPI):
diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py
index 464def533c..4f7b1eb443 100644
--- a/InvenTree/plugin/models.py
+++ b/InvenTree/plugin/models.py
@@ -76,11 +76,22 @@ class PluginConfig(models.Model):
plugin = registry.plugins_full.get(self.key, None)
def get_plugin_meta(name):
+ """Return a meta-value associated with this plugin"""
+
+ # Ignore if the plugin config is not defined
if not plugin:
return None
+
+ # Ignore if the plugin is not active
if not self.active:
- return _('Unvailable')
- return str(getattr(plugin, name, None))
+ return None
+
+ result = getattr(plugin, name, None)
+
+ if result is not None:
+ result = str(result)
+
+ return result
self.meta = {
key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',
diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py
index 4cbc385a21..8d19c1c80e 100644
--- a/InvenTree/plugin/plugin.py
+++ b/InvenTree/plugin/plugin.py
@@ -275,8 +275,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
pub_date = self.package.get('date')
else:
pub_date = datetime.fromisoformat(str(pub_date))
- if not pub_date:
- pub_date = _('No date found') # pragma: no cover
+
return pub_date
@property
diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py
index e5daca6eb3..34ecd7fa66 100644
--- a/InvenTree/plugin/serializers.py
+++ b/InvenTree/plugin/serializers.py
@@ -53,11 +53,20 @@ class PluginConfigSerializer(serializers.ModelSerializer):
"""Meta for serializer."""
model = PluginConfig
fields = [
+ 'pk',
'key',
'name',
'active',
'meta',
'mixins',
+ 'is_builtin',
+ 'is_sample',
+ ]
+
+ read_only_fields = [
+ 'key',
+ 'is_builtin',
+ 'is_sample',
]
meta = serializers.DictField(read_only=True)
@@ -171,6 +180,26 @@ class PluginConfigInstallSerializer(serializers.Serializer):
class PluginConfigEmptySerializer(serializers.Serializer):
"""Serializer for a PluginConfig."""
+ ...
+
+
+class PluginActivateSerializer(serializers.Serializer):
+ """Serializer for activating or deactivating a plugin"""
+
+ model = PluginConfig
+
+ active = serializers.BooleanField(
+ required=False, default=True,
+ label=_('Activate Plugin'),
+ help_text=_('Activate this plugin')
+ )
+
+ def update(self, instance, validated_data):
+ """Apply the new 'active' value to the plugin instance"""
+
+ instance.active = validated_data.get('active', True)
+ instance.save()
+ return instance
class PluginSettingSerializer(GenericReferencedSettingSerializer):
diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html
index 923790ed7f..190f7697f1 100644
--- a/InvenTree/templates/InvenTree/settings/plugin.html
+++ b/InvenTree/templates/InvenTree/settings/plugin.html
@@ -52,37 +52,15 @@
{% endif %}
+
-
-
-
- {% trans "Name" %} |
- {% trans "Key" %} |
- {% trans "Author" %} |
- {% trans "Date" %} |
- {% trans "Version" %} |
- |
-
-
-
-
- {% plugin_list as pl_list %}
- {% if pl_list %}
- {% trans 'Active plugins' %} |
- {% for plugin_key, plugin in pl_list.items %}
- {% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
- {% endfor %}
- {% endif %}
-
- {% inactive_plugin_list as in_pl_list %}
- {% if in_pl_list %}
- {% trans 'Inactive plugins' %} |
- {% for plugin_key, plugin in in_pl_list.items %}
- {% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %}
- {% endfor %}
- {% endif %}
-
-
+
{% plugin_errors as pl_errors %}
diff --git a/InvenTree/templates/InvenTree/settings/plugin_details.html b/InvenTree/templates/InvenTree/settings/plugin_details.html
deleted file mode 100644
index 08090229fc..0000000000
--- a/InvenTree/templates/InvenTree/settings/plugin_details.html
+++ /dev/null
@@ -1,75 +0,0 @@
-{% load inventree_extras %}
-{% load i18n %}
-
-
-
- {% if plugin.is_active %}
-
- {% else %}
-
- {% 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 %}
-
- {% endfor %}
- {% endif %}
-
- {% if plugin.is_builtin %}
-
- {% endif %}
-
- {% if plugin.is_sample %}
-
- {% endif %}
-
- {% if plugin.website %}
-
- {% endif %}
- |
- {{ plugin_key }} |
- {% trans "Unavailable" as no_info %}
-
- {% if plugin.author %}
- {{ plugin.author }}
- {% else %}
- {{ no_info }}
- {% endif %}
- |
-
- {% if plugin.pub_date %}
- {% render_date plugin.pub_date %}
- {% else %}
- {{ no_info }}
- {% endif %}
- |
-
- {% if plugin.version %}
- {{ plugin.version }}
- {% else %}
- {{ no_info }}
- {% endif %}
- |
-
- {% 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 %}
- |
-
diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html
index 14e918a6f0..6a90d5e8c5 100644
--- a/InvenTree/templates/InvenTree/settings/settings.html
+++ b/InvenTree/templates/InvenTree/settings/settings.html
@@ -69,12 +69,6 @@
{% if user.is_staff %}
{% include "InvenTree/settings/settings_staff_js.html" %}
- {% plugins_enabled as plug %}
- {% if plug %}
- $("#install-plugin").click(function() {
- installPlugin();
- });
- {% endif %}
{% endif %}
enableSidebar('settings');
diff --git a/InvenTree/templates/InvenTree/settings/settings_staff_js.html b/InvenTree/templates/InvenTree/settings/settings_staff_js.html
index 7bb5d1da82..aa1b182151 100644
--- a/InvenTree/templates/InvenTree/settings/settings_staff_js.html
+++ b/InvenTree/templates/InvenTree/settings/settings_staff_js.html
@@ -385,3 +385,21 @@ onPanelLoad('stocktake', function() {
});
{% endif %}
});
+
+// Javascript for plugins panel
+onPanelLoad('plugin', function() {
+
+{% plugins_enabled as plug %}
+
+ loadPluginTable('#plugin-table', {
+ custom: {% js_bool plug %},
+ });
+
+{% if plug %}
+ // Callback to install new plugin
+ $("#install-plugin").click(function() {
+ installPlugin();
+ });
+
+{% endif %}
+});
diff --git a/InvenTree/templates/js/translated/plugin.js b/InvenTree/templates/js/translated/plugin.js
index 199d475242..87b1b97c4d 100644
--- a/InvenTree/templates/js/translated/plugin.js
+++ b/InvenTree/templates/js/translated/plugin.js
@@ -2,17 +2,149 @@
{% load inventree_extras %}
/* globals
+ addCachedAlert,
constructForm,
showMessage,
inventreeGet,
inventreePut,
+ loadTableFilters,
+ makeIconButton,
+ renderDate,
+ setupFilterList,
+ showApiError,
+ showModalSpinner,
+ wrapButtons,
*/
/* exported
+ activatePlugin,
installPlugin,
+ loadPluginTable,
locateItemOrLocation
*/
+
+/*
+ * Load the plugin table
+ */
+function loadPluginTable(table, options={}) {
+
+ options.params = options.params || {};
+
+ let filters = loadTableFilters('plugins', options.params);
+
+ setupFilterList('plugins', $(table), '#filter-list-plugins');
+
+ $(table).inventreeTable({
+ url: '{% url "api-plugin-list" %}',
+ name: 'plugins',
+ original: options.params,
+ queryParams: filters,
+ sortable: true,
+ formatNoMatches: function() {
+ return '{% trans "No plugins found" %}';
+ },
+ columns: [
+ {
+ field: 'active',
+ title: '',
+ sortable: true,
+ formatter: function(value, row) {
+ if (row.active) {
+ return ``;
+ } else {
+ return ``;
+ }
+ }
+ },
+ {
+ field: 'name',
+ title: '{% trans "Plugin Description" %}',
+ sortable: true,
+ formatter: function(value, row) {
+ let html = '';
+
+ if (row.active) {
+ html += `${value}`;
+ if (row.meta && row.meta.description) {
+ html += ` - ${row.meta.description}`;
+ }
+ } else {
+ html += `${value}`;
+ }
+
+ if (row.is_builtin) {
+ html += `{% trans "Builtin" %}`;
+ }
+
+ if (row.is_sample) {
+ html += `{% trans "Sample" %}`;
+ }
+
+ return html;
+ }
+ },
+ {
+ field: 'meta.version',
+ title: '{% trans "Version" %}',
+ formatter: function(value, row) {
+ if (value) {
+ let html = value;
+
+ if (row.meta.pub_date) {
+ html += `${renderDate(row.meta.pub_date)}`;
+ }
+
+ return html;
+ } else {
+ return '-';
+ }
+ }
+ },
+ {
+ field: 'meta.author',
+ title: '{% trans "Author" %}',
+ },
+ {
+ field: 'actions',
+ title: '',
+ formatter: function(value, row) {
+ let buttons = '';
+
+ // Check if custom plugins are enabled for this instance
+ if (options.custom && !row.is_builtin) {
+ if (row.active) {
+ buttons += makeIconButton('fa-stop-circle icon-red', 'btn-plugin-disable', row.pk, '{% trans "Disable Plugin" %}');
+ } else {
+ buttons += makeIconButton('fa-play-circle icon-green', 'btn-plugin-enable', row.pk, '{% trans "Enable Plugin" %}');
+ }
+ }
+
+ return wrapButtons(buttons);
+ }
+ },
+ ]
+ });
+
+ if (options.custom) {
+ // Callback to activate a plugin
+ $(table).on('click', '.btn-plugin-enable', function() {
+ let pk = $(this).attr('pk');
+ activatePlugin(pk, true);
+ });
+
+ // Callback to deactivate a plugin
+ $(table).on('click', '.btn-plugin-disable', function() {
+ let pk = $(this).attr('pk');
+ activatePlugin(pk, false);
+ });
+ }
+}
+
+
+/*
+ * Install a new plugin via the API
+ */
function installPlugin() {
constructForm(`/api/plugins/install/`, {
method: 'POST',
@@ -30,6 +162,55 @@ function installPlugin() {
}
+/*
+ * Activate a specific plugin via the API
+ */
+function activatePlugin(plugin_id, active=true) {
+
+ let url = `{% url "api-plugin-list" %}${plugin_id}/activate/`;
+
+ let html = active ? `
+
+ {% trans "Are you sure you want to enable this plugin?" %}
+
+ ` : `
+
+ {% trans "Are you sure you want to disable this plugin?" %}
+
+ `;
+
+ constructForm(null, {
+ title: active ? '{% trans "Enable Plugin" %}' : '{% trans "Disable Plugin" %}',
+ preFormContent: html,
+ confirm: true,
+ submitText: active ? '{% trans "Enable" %}' : '{% trans "Disable" %}',
+ submitClass: active ? 'success' : 'danger',
+ onSubmit: function(_fields, opts) {
+ showModalSpinner(opts.modal);
+
+ inventreePut(
+ url,
+ {
+ active: active,
+ },
+ {
+ method: 'PATCH',
+ success: function() {
+ $(opts.modal).modal('hide');
+ addCachedAlert('{% trans "Plugin updated" %}', {style: 'success'});
+ location.reload();
+ },
+ error: function(xhr) {
+ $(opts.modal).modal('hide');
+ showApiError(xhr, url);
+ }
+ }
+ )
+ }
+ });
+}
+
+
function locateItemOrLocation(options={}) {
if (!options.item && !options.location) {
diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js
index 2ff3569bdf..abeba38d00 100644
--- a/InvenTree/templates/js/translated/table_filters.js
+++ b/InvenTree/templates/js/translated/table_filters.js
@@ -426,6 +426,17 @@ function getPartTestTemplateFilters() {
}
+// Return a dictionary of filters for the "plugins" table
+function getPluginTableFilters() {
+ return {
+ active: {
+ type: 'bool',
+ title: '{% trans "Active" %}',
+ },
+ };
+}
+
+
// Return a dictionary of filters for the "build" table
function getBuildTableFilters() {
@@ -774,6 +785,8 @@ function getAvailableTableFilters(tableKey) {
return getPartTableFilters();
case 'parttests':
return getPartTestTemplateFilters();
+ case 'plugins':
+ return getPluginTableFilters();
case 'purchaseorder':
return getPurchaseOrderFilters();
case 'purchaseorderlineitem':