Repository: inventree/InvenTree

Issue:
[FR] :checkered_flag: Add API endpoint to activate plugins
### Please verify that this feature request has NOT been suggested before.

- [X] I checked and didn't find similar feature request

### Problem statement

There is currently no endpoint to activate a plugin by a key

### Suggested solution

Add an endpoint `/api/plugin/activate` that is supplied with the key/slug of the plugin to enable them

### Describe alternatives you've considered

You could run a patch on the detail endpoint but a separate endpoint would be nice

### Examples of other systems

_No response_

### Do you want to develop this?

- [X] I want to develop this.


Discussion:
@SchrodingersGat what do you think? Also: schould we maybe relocate the detail endpoints for plugins to /plugins instead of /plugin - which also exposes plugins with UrlMixins

Resolving patch:
diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -2,10 +2,12 @@
 
 
 # InvenTree API version
-INVENTREE_API_VERSION = 89
+INVENTREE_API_VERSION = 90
 
 """
 Increment this API version number whenever there is a significant change to the API that any clients need to know about
+v90 -> 2023-01-25 : https://github.com/inventree/InvenTree/pull/4186/files
+    - Adds a dedicated endpoint to activate a plugin
 v89 -> 2023-01-25 : https://github.com/inventree/InvenTree/pull/4214
     - Adds updated field to SupplierPart API
     - Adds API date orddering for supplier part list
diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py
--- a/InvenTree/plugin/api.py
+++ b/InvenTree/plugin/api.py
@@ -10,7 +10,8 @@
 import plugin.serializers as PluginSerializers
 from common.api import GlobalSettingsPermissions
 from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveUpdateAPI,
-                              RetrieveUpdateDestroyAPI)
+                              RetrieveUpdateDestroyAPI, UpdateAPI)
+from InvenTree.permissions import IsSuperuser
 from plugin.base.action.api import ActionPluginView
 from plugin.base.barcodes.api import barcode_api_urls
 from plugin.base.locate.api import LocatePluginView
@@ -122,6 +123,20 @@ def perform_create(self, serializer):
         return serializer.save()
 
 
+class PluginActivate(UpdateAPI):
+    """Endpoint for activating a plugin."""
+
+    queryset = PluginConfig.objects.all()
+    serializer_class = PluginSerializers.PluginConfigEmptySerializer
+    permission_classes = [IsSuperuser, ]
+
+    def perform_update(self, serializer):
+        """Activate the plugin."""
+        instance = serializer.instance
+        instance.active = True
+        instance.save()
+
+
 class PluginSettingList(ListAPI):
     """List endpoint for all plugin related settings.
 
@@ -216,27 +231,24 @@ def get_object(self):
     re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
     re_path(r'^barcode/', include(barcode_api_urls)),
     re_path(r'^locate/', LocatePluginView.as_view(), name='api-locate-plugin'),
+    re_path(r'^plugins/', include([
+        # Plugin settings URLs
+        re_path(r'^settings/', include([
+            re_path(r'^(?P<plugin>\w+)/(?P<key>\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'),
+            re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'),
+        ])),
+
+        # Detail views for a single PluginConfig item
+        re_path(r'^(?P<pk>\d+)/', include([
+            re_path(r'^activate/', PluginActivate.as_view(), name='api-plugin-detail-activate'),
+            re_path(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
+        ])),
+
+        # Plugin managment
+        re_path(r'^install/', PluginInstall.as_view(), name='api-plugin-install'),
+        re_path(r'^activate/', PluginActivate.as_view(), name='api-plugin-activate'),
+
+        # Anything else
+        re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'),
+    ]))
 ]
-
-general_plugin_api_urls = [
-
-    # Plugin settings URLs
-    re_path(r'^settings/', include([
-        re_path(r'^(?P<plugin>\w+)/(?P<key>\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'),
-        re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'),
-    ])),
-
-    # Detail views for a single PluginConfig item
-    re_path(r'^(?P<pk>\d+)/', include([
-        re_path(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
-    ])),
-
-    re_path(r'^install/', PluginInstall.as_view(), name='api-plugin-install'),
-
-    # Anything else
-    re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'),
-]
-
-plugin_api_urls.append(
-    re_path(r'^plugin/', include(general_plugin_api_urls))
-)
diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py
--- a/InvenTree/plugin/serializers.py
+++ b/InvenTree/plugin/serializers.py
@@ -159,6 +159,10 @@ def save(self):
         return ret
 
 
+class PluginConfigEmptySerializer(serializers.Serializer):
+    """Serializer for a PluginConfig."""
+
+
 class PluginSettingSerializer(GenericReferencedSettingSerializer):
     """Serializer for the PluginSetting model."""
 


Test patch:
diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py
--- a/InvenTree/plugin/test_plugin.py
+++ b/InvenTree/plugin/test_plugin.py
@@ -54,7 +54,7 @@ def test_tag_mixin_enabled(self):
     def test_tag_safe_url(self):
         """Test that the safe url tag works expected."""
         # right url
-        self.assertEqual(plugin_tags.safe_url('api-plugin-install'), '/api/plugin/install/')
+        self.assertEqual(plugin_tags.safe_url('api-plugin-install'), '/api/plugins/install/')
         # wrong url
         self.assertEqual(plugin_tags.safe_url('indexas'), None)