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 %} +
+
+
+ {% include "filter_list.html" with id="plugins" %} +
+
+
- - - - - - - - - - - - - - {% plugin_list as pl_list %} - {% if pl_list %} - - {% 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 %} - - {% for plugin_key, plugin in in_pl_list.items %} - {% include "InvenTree/settings/plugin_details.html" with plugin=plugin plugin_key=plugin_key %} - {% endfor %} - {% endif %} - -
{% trans "Name" %}{% trans "Key" %}{% trans "Author" %}{% trans "Date" %}{% trans "Version" %}
{% trans 'Active plugins' %}
{% trans 'Inactive plugins' %}
+
{% 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 %} - - {{ mixin.human_name }} - - {% endfor %} - {% endif %} - - {% if plugin.is_builtin %} - - {% trans "Builtin" %} - - {% endif %} - - {% if plugin.is_sample %} - - {% trans "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':