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

1"""Plugin mixin class for AppMixin.""" 

2 

3from importlib import reload 

4from pathlib import Path 

5from typing import Optional 

6 

7from django.apps import apps 

8from django.conf import settings 

9from django.contrib import admin 

10 

11import structlog 

12 

13from InvenTree.config import get_plugin_dir 

14from plugin import PluginMixinEnum 

15 

16logger = structlog.get_logger('inventree') 

17 

18 

19class AppMixin: 

20 """Mixin that enables full django app functions for a plugin.""" 

21 

22 class MixinMeta: 

23 """Meta options for this mixin.""" 

24 

25 MIXIN_NAME = 'App registration' 

26 

27 def __init__(self): 

28 """Register mixin.""" 

29 super().__init__() 

30 self.add_mixin(PluginMixinEnum.APP, 'has_app', __class__) 

31 

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. 

44 

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 

53 

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 

59 

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 

68 

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) 

82 

83 # rediscover models/ admin sites 

84 cls._reregister_contrib_apps(cls, registry) 

85 

86 # update urls - must be last as models must be registered for creating admin routes 

87 registry._update_urls() 

88 

89 @classmethod 

90 def _deactivate_mixin(cls, registry, force_reload: bool = False): 

91 """Deactivate AppMixin plugins - some magic required. 

92 

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) 

103 

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 

116 

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) 

126 

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) 

132 

133 # remove plugin from installed_apps 

134 registry._clean_installed_apps() 

135 

136 # reset load flag and reload apps 

137 settings.INTEGRATION_APPS_LOADED = False 

138 registry._reload_apps(force_reload=force_reload) 

139 

140 # update urls to remove the apps from the site admin 

141 registry._update_urls() 

142 

143 # region helpers 

144 def _reregister_contrib_apps(self, registry): 

145 """Fix reloading of contrib apps - models and admin. 

146 

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 

158 

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) 

164 

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 

170 

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) 

176 

177 @classmethod 

178 def _get_plugin_path(cls, plugin): 

179 """Parse plugin path. 

180 

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() 

187 

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) 

194 

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] 

200 

201 return plugin_path 

202 

203 # endregion 

204 

205 @property 

206 def has_app(self): 

207 """This plugin is always an app with this plugin.""" 

208 return True