diff --git a/docs/plugins/development/config-templates.md b/docs/plugins/development/config-templates.md index cdcd68b6c4..b2c1d8ac67 100644 --- a/docs/plugins/development/config-templates.md +++ b/docs/plugins/development/config-templates.md @@ -9,11 +9,11 @@ NetBox uses [Jinja](https://jinja.palletsprojects.com/) to render [configuration ## Registering Jinja Filters -### Via `jinja2_env.py` (auto-discovery) +### Via `jinja_env.py` (auto-discovery) -Create a file named `jinja2_env.py` in your plugin root and expose a dict called `filters`. NetBox will auto-discover and register it when the plugin loads. +Create a file named `jinja_env.py` in your plugin root and expose a dict called `filters`. NetBox will auto-discover and register it when the plugin loads. -```python title="my_plugin/jinja2_env.py" +```python title="my_plugin/jinja_env.py" def prefix_list(device): """Return all prefixes assigned to a device's interfaces.""" return [ @@ -35,7 +35,7 @@ The filter is then available in any config template: {% endfor %} ``` -### Via `register_jinja2_filters()` +### Via `register_jinja_filters()` You can also register filters programmatically inside your plugin's `ready()` method: @@ -48,12 +48,12 @@ class MyPluginConfig(PluginConfig): def ready(self): super().ready() - from netbox.plugins.registration import register_jinja2_filters - from .jinja2_env import filters - register_jinja2_filters(filters) + from netbox.plugins.registration import register_jinja_filters + from .jinja_env import filters + register_jinja_filters(filters) ``` -`register_jinja2_filters()` accepts a `dict` mapping filter names to callables. It raises `TypeError` if passed a non-dict or if any value is not callable. +`register_jinja_filters()` accepts a `dict` mapping filter names to callables. It raises `TypeError` if passed a non-dict or if any value is not callable. ### Precedence @@ -81,7 +81,7 @@ JINJA_FILTERS = { ## Injecting Context Variables -Override `get_jinja2_context()` in your `PluginConfig` subclass to inject additional variables into every config template render context. +Override `get_jinja_context()` in your `PluginConfig` subclass to inject additional variables into every config template render context. ```python title="my_plugin/__init__.py" from netbox.plugins import PluginConfig @@ -90,7 +90,7 @@ class MyPluginConfig(PluginConfig): name = 'my_plugin' # ... - def get_jinja2_context(self): + def get_jinja_context(self): from .utils import MyNamespace return { 'my_plugin': MyNamespace(), @@ -104,12 +104,12 @@ The returned dict is merged into the template context, so `my_plugin` becomes av ``` !!! warning "Startup cost" - `get_jinja2_context()` is called on **every** config template render, not once at startup. Keep it fast. Defer expensive lookups to the object you return rather than performing them in `get_jinja2_context()` itself. + `get_jinja_context()` is called on **every** config template render, not once at startup. Keep it fast. Defer expensive lookups to the object you return rather than performing them in `get_jinja_context()` itself. !!! note "Conflict avoidance" Choose context variable names that are unlikely to collide with NetBox's built-in template variables (`device`, `queryset`, etc.) or with those contributed by other plugins. Prefixing with your plugin name is strongly recommended. - In addition, avoid top-level app-label names (`dcim`, `ipam`, `virtualization`, etc.). The auto-populated template context maps each app label to a dict of its public model classes; returning a key like `'dcim'` from `get_jinja2_context()` will silently replace that entire namespace. + In addition, avoid top-level app-label names (`dcim`, `ipam`, `virtualization`, etc.). The auto-populated template context maps each app label to a dict of its public model classes; returning a key like `'dcim'` from `get_jinja_context()` will silently replace that entire namespace. !!! note "No per-render context" - `get_jinja2_context()` receives no arguments — it has no access to the object being rendered or the caller-supplied context. It is intended for plugin-global namespaces (e.g. a lazily-evaluated query helper). Per-object logic belongs in the template itself or in a custom filter. + `get_jinja_context()` receives no arguments — it has no access to the object being rendered or the caller-supplied context. It is intended for plugin-global namespaces (e.g. a lazily-evaluated query helper). Per-object logic belongs in the template itself or in a custom filter. diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md index b7ce27ef68..a605bac346 100644 --- a/docs/plugins/development/index.md +++ b/docs/plugins/development/index.md @@ -119,7 +119,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i | `search_indexes` | The dotted path to the list of search index classes (default: `search.indexes`) | | `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) | | `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | -| `jinja2_filters` | The dotted path to a dict of custom Jinja2 filter functions for use in config templates (default: `jinja2_env.filters`) | +| `jinja_filters` | The dotted path to a dict of custom Jinja filter functions for use in config templates (default: `jinja_env.filters`) | | `menu` | The dotted path to a top-level navigation menu provided by the plugin (default: `navigation.menu`) | | `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | | `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) | diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py index 2b293d1484..af4cd2d634 100644 --- a/netbox/extras/models/mixins.py +++ b/netbox/extras/models/mixins.py @@ -141,9 +141,9 @@ def get_context(self, context=None, queryset=None): for app_config in django_apps.get_app_configs(): if isinstance(app_config, PluginConfig): try: - _context.update(app_config.get_jinja2_context()) + _context.update(app_config.get_jinja_context()) except Exception: - logger.exception("Plugin %r raised an exception in get_jinja2_context()", app_config.name) + logger.exception("Plugin %r raised an exception in get_jinja_context()", app_config.name) if context is not None: _context.update(context) diff --git a/netbox/netbox/plugins/__init__.py b/netbox/netbox/plugins/__init__.py index d5671f17a3..8b46292774 100644 --- a/netbox/netbox/plugins/__init__.py +++ b/netbox/netbox/plugins/__init__.py @@ -20,7 +20,7 @@ registry['plugins'].update({ 'installed': [], 'graphql_schemas': [], - 'jinja2_filters': {}, + 'jinja_filters': {}, 'menus': [], 'menu_items': {}, 'preferences': {}, @@ -31,7 +31,7 @@ 'search_indexes': 'search.indexes', 'data_backends': 'data_backends.backends', 'graphql_schema': 'graphql.schema', - 'jinja2_filters': 'jinja2_env.filters', + 'jinja_filters': 'jinja_env.filters', 'menu': 'navigation.menu', 'menu_items': 'navigation.menu_items', 'template_extensions': 'template_content.template_extensions', @@ -80,7 +80,7 @@ class PluginConfig(AppConfig): search_indexes = None data_backends = None graphql_schema = None - jinja2_filters = None + jinja_filters = None menu = None menu_items = None serializer_resolver = None @@ -88,9 +88,9 @@ class PluginConfig(AppConfig): user_preferences = None events_pipeline = [] - def get_jinja2_context(self): + def get_jinja_context(self): """ - Return a dict of additional variables to inject into the Jinja2 template context + Return a dict of additional variables to inject into the Jinja template context when rendering ConfigTemplates. Override this in a PluginConfig subclass to expose plugin-managed data to config templates without requiring template authors to know internal model names. @@ -133,9 +133,9 @@ def ready(self): for backend in data_backends: register_data_backend()(backend) - # Register Jinja2 filters (if defined) - if jinja2_filters := self._load_resource('jinja2_filters'): - register_jinja2_filters(jinja2_filters) + # Register Jinja filters (if defined) + if jinja_filters := self._load_resource('jinja_filters'): + register_jinja_filters(jinja_filters) # Register template content (if defined) if template_extensions := self._load_resource('template_extensions'): diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py index c5eda45a83..17b5ce79ce 100644 --- a/netbox/netbox/plugins/registration.py +++ b/netbox/netbox/plugins/registration.py @@ -12,7 +12,7 @@ __all__ = ( 'register_graphql_schema', - 'register_jinja2_filters', + 'register_jinja_filters', 'register_menu', 'register_menu_items', 'register_serializer_resolver', @@ -21,24 +21,24 @@ ) -def register_jinja2_filters(filters): +def register_jinja_filters(filters): """ - Register a dict of Jinja2 filter functions provided by a plugin. Each key is the + Register a dict of Jinja filter functions provided by a plugin. Each key is the filter name as it will appear in templates; the value is the callable implementing it. Plugin-registered filters have lower precedence than instance-level JINJA_FILTERS so that site admins can always override them in configuration.py. """ if not isinstance(filters, dict): - raise TypeError(_("jinja2_filters must be a dict mapping filter names to callables")) + raise TypeError(_("jinja_filters must be a dict mapping filter names to callables")) for name, fn in filters.items(): if not callable(fn): - raise TypeError(_("Jinja2 filter '{name}' must be callable").format(name=name)) - if name in registry['plugins']['jinja2_filters']: + raise TypeError(_("Jinja filter '{name}' must be callable").format(name=name)) + if name in registry['plugins']['jinja_filters']: logger.warning( - "Jinja2 filter '%s' registered by a plugin is being overridden by a later-loaded plugin", + "Jinja filter '%s' registered by a plugin is being overridden by a later-loaded plugin", name, ) - registry['plugins']['jinja2_filters'].update(filters) + registry['plugins']['jinja_filters'].update(filters) def register_template_extensions(class_list): diff --git a/netbox/netbox/tests/dummy_plugin/__init__.py b/netbox/netbox/tests/dummy_plugin/__init__.py index 606ae7a56d..488d7ced16 100644 --- a/netbox/netbox/tests/dummy_plugin/__init__.py +++ b/netbox/netbox/tests/dummy_plugin/__init__.py @@ -21,7 +21,7 @@ class DummyPluginConfig(PluginConfig): 'netbox.tests.dummy_plugin.events.process_events_queue' ] - def get_jinja2_context(self): + def get_jinja_context(self): return {'dummy_plugin_var': 'hello_from_dummy'} def ready(self): diff --git a/netbox/netbox/tests/dummy_plugin/jinja2_env.py b/netbox/netbox/tests/dummy_plugin/jinja_env.py similarity index 100% rename from netbox/netbox/tests/dummy_plugin/jinja2_env.py rename to netbox/netbox/tests/dummy_plugin/jinja_env.py diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index d7eb469f6d..6e89eab4a7 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -238,16 +238,16 @@ def test_webhook_callbacks(self): """ self.assertIn(set_context, registry['webhook_callbacks']) - def test_jinja2_filters_registered(self): + def test_jinja_filters_registered(self): """ - Check that Jinja2 filters exported by the dummy plugin are registered in - registry['plugins']['jinja2_filters'] after ready(). + Check that Jinja filters exported by the dummy plugin are registered in + registry['plugins']['jinja_filters'] after ready(). """ - from netbox.tests.dummy_plugin.jinja2_env import dummy_upper - self.assertIn('dummy_upper', registry['plugins']['jinja2_filters']) - self.assertIs(registry['plugins']['jinja2_filters']['dummy_upper'], dummy_upper) + from netbox.tests.dummy_plugin.jinja_env import dummy_upper + self.assertIn('dummy_upper', registry['plugins']['jinja_filters']) + self.assertIs(registry['plugins']['jinja_filters']['dummy_upper'], dummy_upper) - def test_jinja2_filter_available_in_render(self): + def test_jinja_filter_available_in_render(self): """ Filters registered by a plugin must be usable inside render_jinja2(). """ @@ -255,31 +255,31 @@ def test_jinja2_filter_available_in_render(self): result = render_jinja2("{{ 'hello' | dummy_upper }}", {}) self.assertEqual(result, 'HELLO') - def test_get_jinja2_context_merged_into_render(self): + def test_get_jinja_context_merged_into_render(self): """ - Variables returned by a plugin's get_jinja2_context() must appear in the + Variables returned by a plugin's get_jinja_context() must appear in the context produced by RenderTemplateMixin.get_context(). """ from extras.models import ConfigTemplate - ct = ConfigTemplate(name='jinja2-ctx-test', template_code='') + ct = ConfigTemplate(name='jinja-ctx-test', template_code='') ctx = ct.get_context() self.assertIn('dummy_plugin_var', ctx) self.assertEqual(ctx['dummy_plugin_var'], 'hello_from_dummy') - def test_get_jinja2_context_bad_return_is_silenced(self): + def test_get_jinja_context_bad_return_is_silenced(self): """ - A non-dict return from get_jinja2_context() must not crash the render. + A non-dict return from get_jinja_context() must not crash the render. """ from unittest.mock import patch from extras.models import ConfigTemplate from netbox.tests.dummy_plugin import DummyPluginConfig ct = ConfigTemplate(name='bad-ctx-test', template_code='') - with patch.object(DummyPluginConfig, 'get_jinja2_context', return_value='not_a_dict'): + with patch.object(DummyPluginConfig, 'get_jinja_context', return_value='not_a_dict'): ctx = ct.get_context() self.assertNotIn('dummy_plugin_var', ctx) - def test_instance_jinja2_filters_override_plugin_filters(self): + def test_instance_jinja_filters_override_plugin_filters(self): """ Instance-level JINJA_FILTERS must take precedence over plugin-registered filters of the same name. @@ -292,30 +292,30 @@ def test_instance_jinja2_filters_override_plugin_filters(self): @skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") -class PluginJinja2RegistrationTest(TestCase): +class PluginJinjaRegistrationTest(TestCase): """ - Tests for the register_jinja2_filters() registration helper independent of + Tests for the register_jinja_filters() registration helper independent of the dummy plugin's startup path. """ - def test_register_jinja2_filters_rejects_non_dict(self): - from netbox.plugins.registration import register_jinja2_filters + def test_register_jinja_filters_rejects_non_dict(self): + from netbox.plugins.registration import register_jinja_filters with self.assertRaises(TypeError): - register_jinja2_filters([('my_filter', lambda v: v)]) + register_jinja_filters([('my_filter', lambda v: v)]) - def test_register_jinja2_filters_rejects_non_callable_value(self): - from netbox.plugins.registration import register_jinja2_filters + def test_register_jinja_filters_rejects_non_callable_value(self): + from netbox.plugins.registration import register_jinja_filters with self.assertRaises(TypeError): - register_jinja2_filters({'my_filter': 'not_a_function'}) + register_jinja_filters({'my_filter': 'not_a_function'}) - def test_register_jinja2_filters_merges_into_registry(self): - from netbox.plugins.registration import register_jinja2_filters + def test_register_jinja_filters_merges_into_registry(self): + from netbox.plugins.registration import register_jinja_filters fn = lambda v: v # noqa: E731 - register_jinja2_filters({'_test_temp_filter': fn}) + register_jinja_filters({'_test_temp_filter': fn}) try: - self.assertIs(registry['plugins']['jinja2_filters']['_test_temp_filter'], fn) + self.assertIs(registry['plugins']['jinja_filters']['_test_temp_filter'], fn) finally: - del registry['plugins']['jinja2_filters']['_test_temp_filter'] + del registry['plugins']['jinja_filters']['_test_temp_filter'] class PluginNavigationTestCase(TestCase): diff --git a/netbox/utilities/jinja2.py b/netbox/utilities/jinja2.py index 8437b9344b..5ac64c55ec 100644 --- a/netbox/utilities/jinja2.py +++ b/netbox/utilities/jinja2.py @@ -102,7 +102,7 @@ def render_jinja2(template_code, context, environment_params=None, data_file=Non # Instance-level config always wins so site admins can override anything. filters = { **DEFAULT_JINJA2_FILTERS, - **registry['plugins'].get('jinja2_filters', {}), + **registry['plugins'].get('jinja_filters', {}), **get_config().JINJA_FILTERS, } environment.filters.update(filters)