mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -06:00
Merge pull request #10502 from jsenecal/9880-allow-plugins-to-register-apps
Allow Plugins to register a list of Django apps to be appended to INSTALLED_APPS
This commit is contained in:
commit
a454a3f74e
@ -14,6 +14,7 @@ Plugins can do a lot, including:
|
|||||||
* Provide their own "pages" (views) in the web user interface
|
* Provide their own "pages" (views) in the web user interface
|
||||||
* Inject template content and navigation links
|
* Inject template content and navigation links
|
||||||
* Extend NetBox's REST and GraphQL APIs
|
* Extend NetBox's REST and GraphQL APIs
|
||||||
|
* Load additional Django apps
|
||||||
* Add custom request/response middleware
|
* Add custom request/response middleware
|
||||||
|
|
||||||
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
||||||
@ -82,6 +83,7 @@ class FooBarConfig(PluginConfig):
|
|||||||
default_settings = {
|
default_settings = {
|
||||||
'baz': True
|
'baz': True
|
||||||
}
|
}
|
||||||
|
django_apps = ["foo", "bar", "baz"]
|
||||||
|
|
||||||
config = FooBarConfig
|
config = FooBarConfig
|
||||||
```
|
```
|
||||||
@ -101,6 +103,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
|
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
|
||||||
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
|
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
|
||||||
| `default_settings` | A dictionary of configuration parameters and their default values |
|
| `default_settings` | A dictionary of configuration parameters and their default values |
|
||||||
|
| `django_apps` | A list of additional Django apps to load alongside the plugin |
|
||||||
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
|
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
|
||||||
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
||||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||||
@ -112,6 +115,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
|
|
||||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||||
|
|
||||||
|
#### Important Notes About `django_apps`
|
||||||
|
|
||||||
|
Loading additional apps may cause more harm than good and could make identifying problems within NetBox itself more difficult. The `django_apps` attribute is intended only for advanced use cases that require a deeper Django integration.
|
||||||
|
|
||||||
|
Apps from this list are inserted *before* the plugin's `PluginConfig` in the order defined. Adding the plugin's `PluginConfig` module to this list changes this behavior and allows for apps to be loaded *after* the plugin.
|
||||||
|
|
||||||
|
Any additional apps must be installed within the same Python environment as NetBox or `ImproperlyConfigured` exceptions will be raised when loading the plugin.
|
||||||
|
|
||||||
## Create setup.py
|
## Create setup.py
|
||||||
|
|
||||||
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
||||||
|
@ -92,6 +92,9 @@ class DeviceTypeTable(NetBoxTable):
|
|||||||
template_code=DEVICE_WEIGHT,
|
template_code=DEVICE_WEIGHT,
|
||||||
order_by=('_abs_weight', 'weight_unit')
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
)
|
)
|
||||||
|
u_height = columns.TemplateColumn(
|
||||||
|
template_code='{{ value|floatformat }}'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
|
@ -55,6 +55,9 @@ class PluginConfig(AppConfig):
|
|||||||
# Django-rq queues dedicated to the plugin
|
# Django-rq queues dedicated to the plugin
|
||||||
queues = []
|
queues = []
|
||||||
|
|
||||||
|
# Django apps to append to INSTALLED_APPS when plugin requires them.
|
||||||
|
django_apps = []
|
||||||
|
|
||||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||||
# integrated components.
|
# integrated components.
|
||||||
graphql_schema = 'graphql.schema'
|
graphql_schema = 'graphql.schema'
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
import django
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
from django.core.validators import URLValidator
|
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 sentry_sdk.integrations.django import DjangoIntegration
|
||||||
|
|
||||||
from netbox.config import PARAMS
|
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
|
# Monkey patch to fix Django 4.0 support for graphene-django (see
|
||||||
# https://github.com/graphql-python/graphene-django/issues/1284)
|
# https://github.com/graphql-python/graphene-django/issues/1284)
|
||||||
# TODO: Remove this when graphene-django 2.16 becomes available
|
# TODO: Remove this when graphene-django 2.16 becomes available
|
||||||
import django
|
django.utils.encoding.force_text = force_str # type: ignore
|
||||||
from django.utils.encoding import force_str
|
|
||||||
django.utils.encoding.force_text = force_str
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -186,7 +185,7 @@ if STORAGE_BACKEND is not None:
|
|||||||
if STORAGE_BACKEND.startswith('storages.'):
|
if STORAGE_BACKEND.startswith('storages.'):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import storages.utils
|
import storages.utils # type: ignore
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
if getattr(e, 'name') == 'storages':
|
if getattr(e, 'name') == 'storages':
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
@ -663,14 +662,42 @@ for plugin_name in PLUGINS:
|
|||||||
|
|
||||||
# Determine plugin config and add to INSTALLED_APPS.
|
# Determine plugin config and add to INSTALLED_APPS.
|
||||||
try:
|
try:
|
||||||
plugin_config = plugin.config
|
plugin_config: PluginConfig = plugin.config
|
||||||
INSTALLED_APPS.append("{}.{}".format(plugin_config.__module__, plugin_config.__name__))
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Plugin {} does not provide a 'config' variable. This should be defined in the plugin's __init__.py file "
|
"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)
|
"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
|
# Validate user-provided configuration settings and assign defaults
|
||||||
if plugin_name not in PLUGINS_CONFIG:
|
if plugin_name not in PLUGINS_CONFIG:
|
||||||
PLUGINS_CONFIG[plugin_name] = {}
|
PLUGINS_CONFIG[plugin_name] = {}
|
||||||
|
Loading…
Reference in New Issue
Block a user