From 058587d86c7bd4eeeb656f3460a6d1868b0299ae Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 1 Nov 2022 23:14:50 +1100
Subject: [PATCH] Allow loading of "builtin" plugins, even if "plugins" are not
 explicitly loaded

---
 InvenTree/plugin/apps.py     |  49 ++++++++-------
 InvenTree/plugin/plugin.py   |  15 +++++
 InvenTree/plugin/registry.py | 115 +++++++++++++++++------------------
 3 files changed, 96 insertions(+), 83 deletions(-)

diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py
index bd56e05709..28f1acac8e 100644
--- a/InvenTree/plugin/apps.py
+++ b/InvenTree/plugin/apps.py
@@ -7,7 +7,6 @@ The main code for plugin special sauce is in the plugin registry in `InvenTree/p
 import logging
 
 from django.apps import AppConfig
-from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
 from maintenance_mode.core import set_maintenance_mode
@@ -26,34 +25,34 @@ class PluginAppConfig(AppConfig):
 
     def ready(self):
         """The ready method is extended to initialize plugins."""
-        if settings.PLUGINS_ENABLED:
-            if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
-                logger.info("Skipping plugin loading sequence")  # pragma: no cover
-            else:
-                logger.info('Loading InvenTree plugins')
+        if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
+            logger.info("Skipping plugin loading sequence")  # pragma: no cover
+        else:
+            logger.info('Loading InvenTree plugins')
 
-                if not registry.is_loading:
-                    # this is the first startup
-                    try:
-                        from common.models import InvenTreeSetting
-                        if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
-                            # make sure all plugins are installed
-                            registry.install_plugin_file()
-                    except Exception:  # pragma: no cover
-                        pass
+            if not registry.is_loading:
+                # this is the first startup
+                try:
+                    from common.models import InvenTreeSetting
+                    if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
+                        # make sure all plugins are installed
+                        registry.install_plugin_file()
+                except Exception:  # pragma: no cover
+                    pass
 
-                    # get plugins and init them
-                    registry.plugin_modules = registry.collect_plugins()
-                    registry.load_plugins()
+                # get plugins and init them
+                registry.plugin_modules = registry.collect_plugins()
+                registry.load_plugins()
 
-                    # drop out of maintenance
-                    # makes sure we did not have an error in reloading and maintenance is still active
-                    set_maintenance_mode(False)
+                # drop out of maintenance
+                # makes sure we did not have an error in reloading and maintenance is still active
+                set_maintenance_mode(False)
 
-            # check git version
-            registry.git_is_modern = check_git_version()
-            if not registry.git_is_modern:  # pragma: no cover  # simulating old git seems not worth it for coverage
-                log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
+        # check git version
+        registry.git_is_modern = check_git_version()
+
+        if not registry.git_is_modern:  # pragma: no cover  # simulating old git seems not worth it for coverage
+            log_error(_('Your environment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
 
         else:
             logger.info("Plugins not enabled - skipping loading sequence")  # pragma: no cover
diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py
index ece0395092..17a98719e4 100644
--- a/InvenTree/plugin/plugin.py
+++ b/InvenTree/plugin/plugin.py
@@ -106,6 +106,11 @@ class MetaBase:
 
     def is_active(self):
         """Return True if this plugin is currently active."""
+
+        # Builtin plugins are always considered "active"
+        if self.is_builtin():
+            return True
+
         cfg = self.plugin_config()
 
         if cfg:
@@ -300,6 +305,16 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
         """Is this plugin part of the samples?"""
         return self.check_is_sample()
 
+    @classmethod
+    def check_is_builtin(cls) -> bool:
+        """Determine if a particular plugin class is a 'builtin' plugin"""
+        return str(cls.check_package_path()).startswith('plugin/builtin')
+
+    @property
+    def is_builtin(self) -> bool:
+        """Is this plugin is builtin"""
+        return self.check_is_builtin()
+
     @classmethod
     def check_package_path(cls):
         """Path to the plugin."""
diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py
index bc832fa128..d4996ad4ac 100644
--- a/InvenTree/plugin/registry.py
+++ b/InvenTree/plugin/registry.py
@@ -108,9 +108,6 @@ class PluginsRegistry:
         Args:
             full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
         """
-        if not settings.PLUGINS_ENABLED:
-            # Plugins not enabled, do nothing
-            return  # pragma: no cover
 
         logger.info('Start loading plugins')
 
@@ -167,9 +164,6 @@ class PluginsRegistry:
 
     def unload_plugins(self):
         """Unload and deactivate all IntegrationPlugins."""
-        if not settings.PLUGINS_ENABLED:
-            # Plugins not enabled, do nothing
-            return  # pragma: no cover
 
         logger.info('Start unloading plugins')
 
@@ -187,6 +181,7 @@ class PluginsRegistry:
         # remove maintenance
         if not _maintenance:
             set_maintenance_mode(False)  # pragma: no cover
+
         logger.info('Finished unloading plugins')
 
     def reload_plugins(self, full_reload: bool = False):
@@ -210,62 +205,63 @@ class PluginsRegistry:
     def plugin_dirs(self):
         """Construct a list of directories from where plugins can be loaded"""
 
+        # Builtin plugins are *always* loaded
         dirs = ['plugin.builtin', ]
 
-        if settings.TESTING or settings.DEBUG:
-            # If in TEST or DEBUG mode, load plugins from the 'samples' directory
-            dirs.append('plugin.samples')
+        if settings.PLUGINS_ENABLED:
+            # Any 'external' plugins are only loaded if PLUGINS_ENABLED is set to True
 
-        if settings.TESTING:
-            custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
-        else:  # pragma: no cover
-            custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
+            if settings.TESTING or settings.DEBUG:
+                # If in TEST or DEBUG mode, load plugins from the 'samples' directory
+                dirs.append('plugin.samples')
 
-            # Load from user specified directories (unless in testing mode)
-            dirs.append('plugins')
+            if settings.TESTING:
+                custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
+            else:  # pragma: no cover
+                custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
 
-        if custom_dirs is not None:
-            # Allow multiple plugin directories to be specified
-            for pd_text in custom_dirs.split(','):
-                pd = Path(pd_text.strip()).absolute()
+                # Load from user specified directories (unless in testing mode)
+                dirs.append('plugins')
 
-                # Attempt to create the directory if it does not already exist
-                if not pd.exists():
-                    try:
-                        pd.mkdir(exist_ok=True)
-                    except Exception:  # pragma: no cover
-                        logger.error(f"Could not create plugin directory '{pd}'")
-                        continue
+            if custom_dirs is not None:
+                # Allow multiple plugin directories to be specified
+                for pd_text in custom_dirs.split(','):
+                    pd = Path(pd_text.strip()).absolute()
 
-                # Ensure the directory has an __init__.py file
-                init_filename = pd.joinpath('__init__.py')
+                    # Attempt to create the directory if it does not already exist
+                    if not pd.exists():
+                        try:
+                            pd.mkdir(exist_ok=True)
+                        except Exception:  # pragma: no cover
+                            logger.error(f"Could not create plugin directory '{pd}'")
+                            continue
 
-                if not init_filename.exists():
-                    try:
-                        init_filename.write_text("# InvenTree plugin directory\n")
-                    except Exception:  # pragma: no cover
-                        logger.error(f"Could not create file '{init_filename}'")
-                        continue
+                    # Ensure the directory has an __init__.py file
+                    init_filename = pd.joinpath('__init__.py')
 
-                # By this point, we have confirmed that the directory at least exists
-                if pd.exists() and pd.is_dir():
-                    # Convert to python dot-path
-                    if pd.is_relative_to(settings.BASE_DIR):
-                        pd_path = '.'.join(pd.relative_to(settings.BASE_DIR).parts)
-                    else:
-                        pd_path = str(pd)
+                    if not init_filename.exists():
+                        try:
+                            init_filename.write_text("# InvenTree plugin directory\n")
+                        except Exception:  # pragma: no cover
+                            logger.error(f"Could not create file '{init_filename}'")
+                            continue
 
-                    # Add path
-                    dirs.append(pd_path)
-                    logger.info(f"Added plugin directory: '{pd}' as '{pd_path}'")
+                    # By this point, we have confirmed that the directory at least exists
+                    if pd.exists() and pd.is_dir():
+                        # Convert to python dot-path
+                        if pd.is_relative_to(settings.BASE_DIR):
+                            pd_path = '.'.join(pd.relative_to(settings.BASE_DIR).parts)
+                        else:
+                            pd_path = str(pd)
+
+                        # Add path
+                        dirs.append(pd_path)
+                        logger.info(f"Added plugin directory: '{pd}' as '{pd_path}'")
 
         return dirs
 
     def collect_plugins(self):
         """Collect plugins from all possible ways of loading. Returned as list."""
-        if not settings.PLUGINS_ENABLED:
-            # Plugins not enabled, do nothing
-            return  # pragma: no cover
 
         collected_plugins = []
 
@@ -293,17 +289,20 @@ class PluginsRegistry:
             if modules:
                 [collected_plugins.append(item) for item in modules]
 
-        # Check if not running in testing mode and apps should be loaded from hooks
-        if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
-            # Collect plugins from setup entry points
-            for entry in get_entrypoints():
-                try:
-                    plugin = entry.load()
-                    plugin.is_package = True
-                    plugin._get_package_metadata()
-                    collected_plugins.append(plugin)
-                except Exception as error:  # pragma: no cover
-                    handle_error(error, do_raise=False, log_name='discovery')
+        # From this point any plugins are considered "external" and only loaded if plugins are explicitly enabled
+        if settings.PLUGINS_ENABLED:
+
+            # Check if not running in testing mode and apps should be loaded from hooks
+            if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
+                # Collect plugins from setup entry points
+                for entry in get_entrypoints():
+                    try:
+                        plugin = entry.load()
+                        plugin.is_package = True
+                        plugin._get_package_metadata()
+                        collected_plugins.append(plugin)
+                    except Exception as error:  # pragma: no cover
+                        handle_error(error, do_raise=False, log_name='discovery')
 
         # Log collected plugins
         logger.info(f'Collected {len(collected_plugins)} plugins!')