From a58e2e84f8ab07448f73a844e8675107a9a11c66 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 15 Apr 2020 00:16:42 +1000
Subject: [PATCH] Add "ActionPlugin" interface

- Plugin for running a custom action
---
 InvenTree/InvenTree/api.py           | 44 +++++++++++++-
 InvenTree/InvenTree/urls.py          |  7 ++-
 InvenTree/plugins/action/__init__.py |  0
 InvenTree/plugins/action/action.py   | 87 ++++++++++++++++++++++++++++
 InvenTree/plugins/plugin.py          |  2 +-
 InvenTree/plugins/plugins.py         | 28 ++++++++-
 6 files changed, 161 insertions(+), 7 deletions(-)
 create mode 100644 InvenTree/plugins/action/__init__.py
 create mode 100644 InvenTree/plugins/action/action.py

diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index af66fa6751..4103fd290a 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -8,6 +8,7 @@ from __future__ import unicode_literals
 from django.utils.translation import ugettext as _
 from django.http import JsonResponse
 
+from rest_framework import permissions
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
@@ -20,6 +21,7 @@ from plugins import plugins as inventree_plugins
 print("INFO: Loading plugins")
 
 barcode_plugins = inventree_plugins.load_barcode_plugins()
+action_plugins = inventree_plugins.load_action_plugins()
 
 
 class InfoView(AjaxView):
@@ -38,7 +40,43 @@ class InfoView(AjaxView):
         return JsonResponse(data)
 
 
-class BarcodeScanView(APIView):
+class ActionPluginView(APIView):
+    """
+    Endpoint for running custom action plugins.
+    """
+
+    permission_classes = [
+        permissions.IsAuthenticated,
+    ]
+
+    def post(self, request, *args, **kwargs):
+
+        action = request.data.get('action', None)
+
+        data = request.data.get('data', None)
+
+        if action is None:
+            return Response({
+                'error': _("No action specified")
+            })
+
+        for plugin_class in action_plugins:
+            if plugin_class.action_name() == action:
+
+                plugin = plugin_class(request.user, data=data)
+
+                plugin.perform_action()
+
+                return Response(plugin.get_response())
+
+        # If we got to here, no matching action was found
+        return Response({
+            'error': _("No matching action found for"),
+            "action": action,
+        })
+
+
+class BarcodePluginView(APIView):
     """
     Endpoint for handling barcode scan requests.
 
@@ -50,6 +88,10 @@ class BarcodeScanView(APIView):
 
     """
 
+    permission_classes = [
+        permissions.IsAuthenticated,
+    ]
+
     def post(self, request, *args, **kwargs):
 
         response = {}
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 1d1fabc795..d9600333f4 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -36,7 +36,7 @@ from rest_framework.documentation import include_docs_urls
 from .views import IndexView, SearchView, DatabaseStatsView
 from .views import SettingsView, EditUserView, SetPasswordView
 
-from .api import InfoView, BarcodeScanView
+from .api import InfoView, BarcodePluginView, ActionPluginView
 
 from users.urls import user_urls
 
@@ -54,8 +54,9 @@ apipatterns = [
     # User URLs
     url(r'^user/', include(user_urls)),
 
-    # Barcode scanning endpoint
-    url(r'^barcode/', BarcodeScanView.as_view(), name='api-barcode-scan'),
+    # Plugin endpoints
+    url(r'^barcode/', BarcodePluginView.as_view(), name='api-barcode-plugin'),
+    url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
 
     # InvenTree information endpoint
     url(r'^$', InfoView.as_view(), name='api-inventree-info'),
diff --git a/InvenTree/plugins/action/__init__.py b/InvenTree/plugins/action/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py
new file mode 100644
index 0000000000..4e0b0f5cb0
--- /dev/null
+++ b/InvenTree/plugins/action/action.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+import plugins.plugin as plugin
+
+
+class ActionPlugin(plugin.InvenTreePlugin):
+    """
+    The ActionPlugin class is used to perform custom actions
+    """
+
+    ACTION_NAME = ""
+
+    @classmethod
+    def action_name(cls):
+        """
+        Return the action name for this plugin.
+        If the ACTION_NAME parameter is empty,
+        look at the PLUGIN_NAME instead.
+        """
+        action = cls.ACTION_NAME
+        
+        if not action:
+            action = cls.PLUGIN_NAME
+        
+        return action
+
+    def __init__(self, user, data=None):
+        """
+        An action plugin takes a user reference, and an optional dataset (dict)
+        """
+        plugin.InvenTreePlugin.__init__(self)
+
+        self.user = user
+        self.data = data
+
+    def perform_action(self):
+        """
+        Override this method to perform the action!
+        """
+        pass
+
+    def get_result(self):
+        """
+        Result of the action?
+        """
+
+        # Re-implement this for cutsom actions
+        return False
+
+    def get_info(self):
+        """
+        Extra info? Can be a string / dict / etc
+        """
+        return None
+
+    def get_response(self):
+        """
+        Return a response. Default implementation is a simple response
+        which can be overridden.
+        """
+        return {
+            "action": self.action_name(),
+            "result": self.get_result(),
+            "info": self.get_info(),
+        }
+
+
+class SimpleActionPlugin(ActionPlugin):
+    """
+    An EXTREMELY simple action plugin which demonstrates
+    the capability of the ActionPlugin class
+    """
+
+    PLUGIN_NAME = "SimpleActionPlugin"
+    ACTION_NAME = "simple"
+
+    def perform_action(self):
+        print("Action plugin in action!")
+
+    def get_info(self):
+        return {
+            "user": self.user.username,
+            "hello": "world",
+        }
+
+    def get_result(self):
+        return True
diff --git a/InvenTree/plugins/plugin.py b/InvenTree/plugins/plugin.py
index ec40b6d4cf..11de4d1365 100644
--- a/InvenTree/plugins/plugin.py
+++ b/InvenTree/plugins/plugin.py
@@ -9,7 +9,7 @@ class InvenTreePlugin():
     # Override the plugin name for each concrete plugin instance
     PLUGIN_NAME = ''
 
-    def get_name(self):
+    def plugin_name(self):
         return self.PLUGIN_NAME
 
     def __init__(self):
diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py
index 03e127933e..f913c1f295 100644
--- a/InvenTree/plugins/plugins.py
+++ b/InvenTree/plugins/plugins.py
@@ -8,6 +8,10 @@ import pkgutil
 import plugins.barcode as barcode
 from plugins.barcode.barcode import BarcodePlugin
 
+# Action plugins
+import plugins.action as action
+from plugins.action.action import ActionPlugin
+
 
 def iter_namespace(pkg):
 
@@ -16,7 +20,7 @@ def iter_namespace(pkg):
 
 def get_modules(pkg):
     # Return all modules in a given package
-    return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(barcode)]
+    return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)]
 
 
 def get_classes(module):
@@ -41,7 +45,7 @@ def get_plugins(pkg, baseclass):
         # Iterate through each class in the module
         for item in get_classes(mod):
             plugin = item[1]
-            if plugin.__class__ is type(baseclass) and plugin.PLUGIN_NAME:
+            if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME:
                 plugins.append(plugin)
 
     return plugins
@@ -52,6 +56,8 @@ def load_barcode_plugins():
     Return a list of all registered barcode plugins
     """
 
+    print("Loading barcode plugins")
+
     plugins = get_plugins(barcode, BarcodePlugin)
 
     if len(plugins) > 0:
@@ -61,3 +67,21 @@ def load_barcode_plugins():
             print(" - {bp}".format(bp=bp.PLUGIN_NAME))
 
     return plugins
+
+
+def load_action_plugins():
+    """
+    Return a list of all registered action plugins
+    """
+
+    print("Loading action plugins")
+
+    plugins = get_plugins(action, ActionPlugin)
+
+    if len(plugins) > 0:
+        print("Discovered {n} action plugins:".format(n=len(plugins)))
+
+        for ap in plugins:
+            print(" - {ap}".format(ap=ap.PLUGIN_NAME))
+
+    return plugins