|
|
@@ -1,18 +1,19 @@
|
|
|
import hashlib
|
|
|
import importlib
|
|
|
-import logging
|
|
|
+import importlib.util
|
|
|
import os
|
|
|
import platform
|
|
|
-import re
|
|
|
-import socket
|
|
|
import sys
|
|
|
import warnings
|
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
+import django
|
|
|
import sentry_sdk
|
|
|
from django.contrib.messages import constants as messages
|
|
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
|
from django.core.validators import URLValidator
|
|
|
+from django.utils.encoding import force_str
|
|
|
+from extras.plugins import PluginConfig
|
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
|
|
|
|
from netbox.config import PARAMS
|
|
|
@@ -20,9 +21,7 @@ from netbox.config import PARAMS
|
|
|
# Monkey patch to fix Django 4.0 support for graphene-django (see
|
|
|
# https://github.com/graphql-python/graphene-django/issues/1284)
|
|
|
# TODO: Remove this when graphene-django 2.16 becomes available
|
|
|
-import django
|
|
|
-from django.utils.encoding import force_str
|
|
|
-django.utils.encoding.force_text = force_str
|
|
|
+django.utils.encoding.force_text = force_str # type: ignore
|
|
|
|
|
|
|
|
|
#
|
|
|
@@ -186,7 +185,7 @@ if STORAGE_BACKEND is not None:
|
|
|
if STORAGE_BACKEND.startswith('storages.'):
|
|
|
|
|
|
try:
|
|
|
- import storages.utils
|
|
|
+ import storages.utils # type: ignore
|
|
|
except ModuleNotFoundError as e:
|
|
|
if getattr(e, 'name') == 'storages':
|
|
|
raise ImproperlyConfigured(
|
|
|
@@ -663,14 +662,42 @@ for plugin_name in PLUGINS:
|
|
|
|
|
|
# Determine plugin config and add to INSTALLED_APPS.
|
|
|
try:
|
|
|
- plugin_config = plugin.config
|
|
|
- INSTALLED_APPS.append("{}.{}".format(plugin_config.__module__, plugin_config.__name__))
|
|
|
+ plugin_config: PluginConfig = plugin.config
|
|
|
except AttributeError:
|
|
|
raise ImproperlyConfigured(
|
|
|
"Plugin {} does not provide a 'config' variable. This should be defined in the plugin's __init__.py file "
|
|
|
"and point to the PluginConfig subclass.".format(plugin_name)
|
|
|
)
|
|
|
|
|
|
+ plugin_module = "{}.{}".format(plugin_config.__module__, plugin_config.__name__) # type: ignore
|
|
|
+
|
|
|
+ # Gather additional apps to load alongside this plugin
|
|
|
+ django_apps = plugin_config.django_apps
|
|
|
+ if plugin_name in django_apps:
|
|
|
+ django_apps.pop(plugin_name)
|
|
|
+ if plugin_module not in django_apps:
|
|
|
+ django_apps.append(plugin_module)
|
|
|
+
|
|
|
+ # Test if we can import all modules (or its parent, for PluginConfigs and AppConfigs)
|
|
|
+ for app in django_apps:
|
|
|
+ if "." in app:
|
|
|
+ parts = app.split(".")
|
|
|
+ spec = importlib.util.find_spec(".".join(parts[:-1]))
|
|
|
+ else:
|
|
|
+ spec = importlib.util.find_spec(app)
|
|
|
+ if spec is None:
|
|
|
+ raise ImproperlyConfigured(
|
|
|
+ f"Failed to load django_apps specified by plugin {plugin_name}: {django_apps} "
|
|
|
+ f"The module {app} cannot be imported. Check that the necessary package has been "
|
|
|
+ "installed within the correct Python environment."
|
|
|
+ )
|
|
|
+
|
|
|
+ INSTALLED_APPS.extend(django_apps)
|
|
|
+
|
|
|
+ # Preserve uniqueness of the INSTALLED_APPS list, we keep the last occurence
|
|
|
+ sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
|
|
|
+ INSTALLED_APPS = list(sorted_apps)
|
|
|
+
|
|
|
# Validate user-provided configuration settings and assign defaults
|
|
|
if plugin_name not in PLUGINS_CONFIG:
|
|
|
PLUGINS_CONFIG[plugin_name] = {}
|