Coverage for src/backend/InvenTree/plugin/base/integration/AppMixin.py: 26%
90 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Plugin mixin class for AppMixin."""
3from importlib import reload
4from pathlib import Path
5from typing import Optional
7from django.apps import apps
8from django.conf import settings
9from django.contrib import admin
11import structlog
13from InvenTree.config import get_plugin_dir
14from plugin import PluginMixinEnum
16logger = structlog.get_logger('inventree')
19class AppMixin:
20 """Mixin that enables full django app functions for a plugin."""
22 class MixinMeta:
23 """Meta options for this mixin."""
25 MIXIN_NAME = 'App registration'
27 def __init__(self):
28 """Register mixin."""
29 super().__init__()
30 self.add_mixin(PluginMixinEnum.APP, 'has_app', __class__)
32 @classmethod
33 def _activate_mixin(
34 cls,
35 registry,
36 plugins,
37 force_reload=False,
38 full_reload: bool = False,
39 _internal: Optional[list] = None,
40 *args,
41 **kwargs,
42 ):
43 """Activate AppMixin plugins - add custom apps and reload.
45 Args:
46 registry (PluginRegistry): The registry that should be used
47 plugins (dict): List of IntegrationPlugins that should be installed
48 force_reload (bool, optional): Only reload base apps. Defaults to False.
49 full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
50 _internal (dict, optional): Internal use only (for testing). Defaults to None.
51 """
52 from common.settings import get_global_setting
54 if settings.PLUGIN_TESTING or get_global_setting( 54 ↛ 57line 54 didn't jump to line 57 because the condition on line 54 was never true
55 'ENABLE_PLUGINS_APP', create=False
56 ):
57 logger.info('Registering IntegrationPlugin apps')
58 apps_changed = False
60 # add them to the INSTALLED_APPS
61 for _key, plugin in plugins:
62 if plugin.mixin_enabled(PluginMixinEnum.APP):
63 plugin_path = cls._get_plugin_path(plugin)
64 if plugin_path not in settings.INSTALLED_APPS:
65 settings.INSTALLED_APPS += [plugin_path]
66 registry.installed_apps += [plugin_path]
67 apps_changed = True
69 # if apps were changed or force loading base apps -> reload
70 # Ignore reloading if we are in testing mode and apps are unchanged so that tests run faster
71 # registry.reload_plugins(...) first unloads and then loads the plugins
72 # always reload if we are not in testing mode so we can expect the second reload
73 if not settings.TESTING or apps_changed or force_reload:
74 # first startup or force loading of base apps -> registry is prob false
75 if registry.apps_loading or force_reload:
76 registry.apps_loading = False
77 registry._reload_apps(
78 force_reload=True, full_reload=full_reload, _internal=_internal
79 )
80 else:
81 registry._reload_apps(full_reload=full_reload, _internal=_internal)
83 # rediscover models/ admin sites
84 cls._reregister_contrib_apps(cls, registry)
86 # update urls - must be last as models must be registered for creating admin routes
87 registry._update_urls()
89 @classmethod
90 def _deactivate_mixin(cls, registry, force_reload: bool = False):
91 """Deactivate AppMixin plugins - some magic required.
93 Args:
94 registry (PluginRegistry): The registry that should be used
95 force_reload (bool, optional): Also reload base apps. Defaults to False.
96 """
97 # unregister models from admin
98 for plugin_path in registry.installed_apps: 98 ↛ 99line 98 didn't jump to line 99 because the loop on line 98 never started
99 models = [] # the modelrefs need to be collected as popping an item in a iter is not welcomed
100 app_name = plugin_path.split('.')[-1]
101 try:
102 app_config = apps.get_app_config(app_name)
104 # check all models
105 for model in app_config.get_models():
106 # remove model from admin site
107 try:
108 admin.site.unregister(model)
109 except Exception: # pragma: no cover
110 pass
111 models += [model._meta.model_name]
112 except LookupError: # pragma: no cover
113 # if an error occurs the app was never loaded right -> so nothing to do anymore
114 logger.debug('%s App was not found during deregistering', app_name)
115 break
117 # unregister the models (yes, models are just kept in multilevel dicts)
118 # Note: Django registers models under the app_label (app_name),
119 # not the full dotted plugin_path. For plugins with nested module
120 # paths (e.g. "myplugin.myplugin"), plugin_path != app_name, so
121 # using plugin_path here would look up the wrong key in the
122 # defaultdict and raise KeyError on .pop().
123 for model in models:
124 # remove model from general registry
125 apps.all_models[app_name].pop(model, None)
127 # clear the registry for that app
128 # so that the import trick will work on reloading the same plugin
129 # -> the registry is kept for the whole lifecycle
130 if models and app_name in apps.all_models:
131 apps.all_models.pop(app_name)
133 # remove plugin from installed_apps
134 registry._clean_installed_apps()
136 # reset load flag and reload apps
137 settings.INTEGRATION_APPS_LOADED = False
138 registry._reload_apps(force_reload=force_reload)
140 # update urls to remove the apps from the site admin
141 registry._update_urls()
143 # region helpers
144 def _reregister_contrib_apps(self, registry):
145 """Fix reloading of contrib apps - models and admin.
147 This is needed if plugins were loaded earlier and then reloaded as models and admins rely on imports.
148 Those register models and admin in their respective objects (e.g. admin.site for admin).
149 """
150 for plugin_path in registry.installed_apps:
151 try:
152 app_name = plugin_path.split('.')[-1]
153 app_config = apps.get_app_config(app_name)
154 except LookupError: # pragma: no cover
155 # the plugin was never loaded correctly
156 logger.debug('%s App was not found during deregistering', app_name)
157 break
159 # reload models if they were set
160 # models_module gets set if models were defined - even after multiple loads
161 # on a reload the models registry is empty but models_module is not
162 if app_config.models_module and len(app_config.models) == 0:
163 reload(app_config.models_module)
165 # check for all models if they are registered with the site admin
166 model_not_reg = False
167 for model in app_config.get_models():
168 if not admin.site.is_registered(model):
169 model_not_reg = True
171 # reload admin if at least one model is not registered
172 # models are registered with admin in the 'admin.py' file - so we check
173 # if the app_config has an admin module before trying to load it
174 if model_not_reg and hasattr(app_config.module, 'admin'):
175 reload(app_config.module.admin)
177 @classmethod
178 def _get_plugin_path(cls, plugin):
179 """Parse plugin path.
181 The input can be either:
182 - a local file / dir
183 - a package
184 """
185 path = plugin.path()
186 custom_plugins_dir = get_plugin_dir()
188 if path.is_relative_to(settings.BASE_DIR):
189 # Plugins which are located relative to the base code directory
190 plugin_path = '.'.join(path.relative_to(settings.BASE_DIR).parts)
191 elif custom_plugins_dir and path.is_relative_to(custom_plugins_dir):
192 # Plugins which are located relative to the custom plugins directory
193 plugin_path = '.'.join(path.relative_to(custom_plugins_dir).parts)
195 # Ensure that the parent directory is added also
196 plugin_path = Path(custom_plugins_dir).parts[-1] + '.' + plugin_path
197 else:
198 # plugin is shipped as package - extract plugin module name
199 plugin_path = plugin.__module__.split('.')[0]
201 return plugin_path
203 # endregion
205 @property
206 def has_app(self):
207 """This plugin is always an app with this plugin."""
208 return True