From 47f341d2b5c400464bf444940b4222245afaaf08 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 20 Sep 2023 13:32:34 +1000 Subject: [PATCH] Plugin loading fixes (#5572) * Add config function to return external plugins dir * Enable AppMixin support relative to external plugins directory * Fix for urls.py - URL patterns were causing custom app mixin plugins to fail reverse lookup in admin interface - Brought admin URLs up one level * simplify urls.py * Fix plugin registry code which registers plugin URLs - As we have updated InvenTree.urls.py we need to adjust this logic too * Adds redirect for favicon.ico * Handle empty plugins dir --- InvenTree/InvenTree/config.py | 6 +++ InvenTree/InvenTree/urls.py | 37 +++++++++++-------- InvenTree/plugin/base/integration/AppMixin.py | 21 +++++++++-- InvenTree/plugin/registry.py | 35 +++++++++++------- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py index 1ddbc80d6c..4e54f62a19 100644 --- a/InvenTree/InvenTree/config.py +++ b/InvenTree/InvenTree/config.py @@ -281,6 +281,12 @@ def get_plugin_file(): return plugin_file +def get_plugin_dir(): + """Returns the path of the custom plugins directory""" + + return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir') + + def get_secret_key(): """Return the secret key value which will be used by django. diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index a7cba2fd97..a7652f8969 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -189,10 +189,6 @@ classic_frontendpatterns = [ re_path(r'^about/', AboutView.as_view(), name='about'), re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'), - # admin sites - re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')), - re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'), - # DB user sessions path('accounts/sessions/other/delete/', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), re_path(r'^accounts/sessions/(?P\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ), @@ -213,22 +209,26 @@ classic_frontendpatterns = [ new_frontendpatterns = platform_urls -# Load patterns for frontend according to settings -frontendpatterns = [] -if settings.ENABLE_CLASSIC_FRONTEND: - frontendpatterns.append(re_path('', include(classic_frontendpatterns))) -if settings.ENABLE_PLATFORM_FRONTEND: - frontendpatterns.append(re_path('', include(new_frontendpatterns))) +urlpatterns = [ + # admin sites + re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')), + re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'), +] +urlpatterns += backendpatterns + +frontendpatterns = [] + +if settings.ENABLE_CLASSIC_FRONTEND: + frontendpatterns += classic_frontendpatterns +if settings.ENABLE_PLATFORM_FRONTEND: + frontendpatterns += new_frontendpatterns + +urlpatterns += frontendpatterns # Append custom plugin URLs (if plugin support is enabled) if settings.PLUGINS_ENABLED: - frontendpatterns.append(get_plugin_urls()) - -urlpatterns = [ - re_path('', include(frontendpatterns)), - re_path('', include(backendpatterns)), -] + urlpatterns.append(get_plugin_urls()) # Server running in "DEBUG" mode? if settings.DEBUG: @@ -245,5 +245,10 @@ if settings.DEBUG: path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns +# Redirect for favicon.ico +urlpatterns.append( + path('favicon.ico', RedirectView.as_view(url=f'{settings.STATIC_ROOT}/img/favicon/favicon.ico')) +) + # Send any unknown URLs to the parts page urlpatterns += [re_path(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')] diff --git a/InvenTree/plugin/base/integration/AppMixin.py b/InvenTree/plugin/base/integration/AppMixin.py index 4334dabaaf..8c3ae557eb 100644 --- a/InvenTree/plugin/base/integration/AppMixin.py +++ b/InvenTree/plugin/base/integration/AppMixin.py @@ -1,11 +1,14 @@ """Plugin mixin class for AppMixin.""" import logging from importlib import reload +from pathlib import Path from django.apps import apps from django.conf import settings from django.contrib import admin +from InvenTree.config import get_plugin_dir + logger = logging.getLogger('inventree') @@ -156,12 +159,22 @@ class AppMixin: - a local file / dir - a package """ - try: - # for local path plugins - plugin_path = '.'.join(plugin.path().relative_to(settings.BASE_DIR).parts) - except ValueError: # pragma: no cover + path = plugin.path() + custom_plugins_dir = get_plugin_dir() + + if path.is_relative_to(settings.BASE_DIR): + # Plugins which are located relative to the base code directory + plugin_path = '.'.join(path.relative_to(settings.BASE_DIR).parts) + elif custom_plugins_dir and path.is_relative_to(custom_plugins_dir): + # Plugins which are located relative to the custom plugins directory + plugin_path = '.'.join(path.relative_to(custom_plugins_dir).parts) + + # Ensure that the parent directory is added also + plugin_path = Path(custom_plugins_dir).parts[-1] + '.' + plugin_path + else: # plugin is shipped as package - extract plugin module name plugin_path = plugin.__module__.split('.')[0] + return plugin_path # endregion diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 3fd402d471..999765edb8 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -17,14 +17,14 @@ from django.apps import apps from django.conf import settings from django.contrib import admin from django.db.utils import IntegrityError, OperationalError, ProgrammingError -from django.urls import clear_url_caches, include, re_path +from django.urls import clear_url_caches, re_path from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from maintenance_mode.core import (get_maintenance_mode, maintenance_mode_on, set_maintenance_mode) -from InvenTree.config import get_setting +from InvenTree.config import get_plugin_dir from InvenTree.ready import canAppAccessDatabase from .helpers import (IntegrationPluginError, get_entrypoints, get_plugins, @@ -239,7 +239,7 @@ class PluginsRegistry: 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') + custom_dirs = get_plugin_dir() # Load from user specified directories (unless in testing mode) dirs.append('plugins') @@ -577,19 +577,28 @@ class PluginsRegistry: self.plugins_full: Dict[str, InvenTreePlugin] = {} def _update_urls(self): - from InvenTree.urls import frontendpatterns as urlpattern - from InvenTree.urls import urlpatterns as global_pattern + """Due to the order in which plugins are loaded, the patterns in urls.py may be out of date. + + This function updates the patterns in urls.py to ensure that the correct patterns are loaded, + and then refreshes the django url cache. + + Note that we also have to refresh the admin site URLS, + as any custom AppMixin plugins require admin integration + """ + + from InvenTree.urls import urlpatterns from plugin.urls import get_plugin_urls - for index, url in enumerate(urlpattern): - if hasattr(url, 'app_name'): - if url.app_name == 'admin': - urlpattern[index] = re_path(r'^admin/', admin.site.urls, name='inventree-admin') - elif url.app_name == 'plugin': - urlpattern[index] = get_plugin_urls() + for index, url in enumerate(urlpatterns): - # Replace frontendpatterns - global_pattern[0] = re_path('', include(urlpattern)) + app_name = getattr(url, 'app_name', None) + + if app_name == 'admin': + urlpatterns[index] = re_path(r'^admin/', admin.site.urls, name='inventree-admin') + if app_name == 'plugin': + urlpatterns[index] = get_plugin_urls() + + # Refresh the URL cache clear_url_caches() # endregion