netbox/netbox/utilities/jinja2.py
Jason Novinger d7672ab260 Fixes #19490: restores nesting behavior of DataSource-based ConfigTemplates
The ability to render nested templates was accidentally removed with the
implementation of #17653, which normalized the behavior of various Jinja2
template rendering actions.

This fix restores that behavior while retaining the normalized behavior.
This fix also includes regression tests to ensure this behavior is not
removed accidentally again in the future.
2025-05-23 16:34:22 -05:00

76 lines
2.4 KiB
Python

from django.apps import apps
from jinja2 import BaseLoader, TemplateNotFound
from jinja2.meta import find_referenced_templates
from jinja2.sandbox import SandboxedEnvironment
from netbox.config import get_config
__all__ = (
'DataFileLoader',
'render_jinja2',
)
class DataFileLoader(BaseLoader):
"""
Custom Jinja2 loader to facilitate populating template content from DataFiles.
"""
def __init__(self, data_source):
self.data_source = data_source
self._template_cache = {}
def get_source(self, environment, template):
DataFile = apps.get_model('core', 'DataFile')
# Retrieve template content from cache
try:
template_source = self._template_cache[template]
except KeyError:
raise TemplateNotFound(template)
# Find and pre-fetch referenced templates
if referenced_templates := tuple(find_referenced_templates(environment.parse(template_source))):
related_files = DataFile.objects.filter(source=self.data_source)
# None indicates the use of dynamic resolution. If dependent files are statically
# defined, we can filter by path for optimization.
if None not in referenced_templates:
related_files = related_files.filter(path__in=referenced_templates)
self.cache_templates({
df.path: df.data_as_string for df in related_files
})
return template_source, template, lambda: True
def cache_templates(self, templates):
self._template_cache.update(templates)
#
# Utility functions
#
def render_jinja2(template_code, context, environment_params=None, data_file=None):
"""
Render a Jinja2 template with the provided context. Return the rendered content.
"""
environment_params = environment_params or {}
if 'loader' not in environment_params:
if data_file:
loader = DataFileLoader(data_file.source)
loader.cache_templates({
data_file.path: template_code
})
else:
loader = BaseLoader()
environment_params['loader'] = loader
environment = SandboxedEnvironment(**environment_params)
environment.filters.update(get_config().JINJA2_FILTERS)
if data_file:
template = environment.get_template(data_file.path)
else:
template = environment.from_string(source=template_code)
return template.render(**context)