소스 검색

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.
Jason Novinger 8 달 전
부모
커밋
d7672ab260
3개의 변경된 파일90개의 추가작업 그리고 8개의 파일을 삭제
  1. 1 1
      netbox/extras/models/mixins.py
  2. 71 5
      netbox/extras/tests/test_models.py
  3. 18 2
      netbox/utilities/jinja2.py

+ 1 - 1
netbox/extras/models/mixins.py

@@ -131,7 +131,7 @@ class RenderTemplateMixin(models.Model):
         """
         context = self.get_context(context=context, queryset=queryset)
         env_params = self.environment_params or {}
-        output = render_jinja2(self.template_code, context, env_params)
+        output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
 
         # Replace CRLF-style line terminators
         output = output.replace('\r\n', '\n')

+ 71 - 5
netbox/extras/tests/test_models.py

@@ -1,9 +1,12 @@
+import tempfile
+from pathlib import Path
+
 from django.forms import ValidationError
-from django.test import TestCase
+from django.test import tag, TestCase
 
-from core.models import ObjectType
+from core.models import DataSource, ObjectType
 from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
-from extras.models import ConfigContext, Tag
+from extras.models import ConfigContext, ConfigTemplate, Tag
 from tenancy.models import Tenant, TenantGroup
 from utilities.exceptions import AbortRequest
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -33,8 +36,8 @@ class TagTest(TestCase):
         ]
 
         site = Site.objects.create(name='Site 1')
-        for tag in tags:
-            site.tags.add(tag)
+        for _tag in tags:
+            site.tags.add(_tag)
         site.save()
 
         site = Site.objects.first()
@@ -540,3 +543,66 @@ class ConfigContextTest(TestCase):
         device.local_context_data = 'foo'
         with self.assertRaises(ValidationError):
             device.clean()
+
+
+class ConfigTemplateTest(TestCase):
+    """
+    TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data.
+    """
+    MAIN_TEMPLATE = """
+    {%- include 'base.j2' %}
+    """.strip()
+    BASE_TEMPLATE = """
+    Hi
+    """.strip()
+
+    @classmethod
+    def _create_template_file(cls, templates_dir, file_name, content):
+        template_file_name = file_name
+        if not template_file_name.endswith('j2'):
+            template_file_name += '.j2'
+        temp_file_path = templates_dir / template_file_name
+
+        with open(temp_file_path, 'w') as f:
+            f.write(content)
+
+    @classmethod
+    def setUpTestData(cls):
+        temp_dir = tempfile.TemporaryDirectory()
+        templates_dir = Path(temp_dir.name) / "templates"
+        templates_dir.mkdir(parents=True, exist_ok=True)
+
+        cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE)
+        cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE)
+
+        data_source = DataSource(
+            name="Test DataSource",
+            type="local",
+            source_url=str(templates_dir),
+        )
+        data_source.save()
+        data_source.sync()
+
+        base_config_template = ConfigTemplate(
+            name="BaseTemplate",
+            data_file=data_source.datafiles.filter(path__endswith='base.j2').first()
+        )
+        base_config_template.clean()
+        base_config_template.save()
+        cls.base_config_template = base_config_template
+
+        main_config_template = ConfigTemplate(
+            name="MainTemplate",
+            data_file=data_source.datafiles.filter(path__endswith='main.j2').first()
+        )
+        main_config_template.clean()
+        main_config_template.save()
+        cls.main_config_template = main_config_template
+
+    @tag('regression')
+    def test_config_template_with_data_source(self):
+        self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({}))
+
+    @tag('regression')
+    def test_config_template_with_data_source_nested_templates(self):
+        self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))

+ 18 - 2
netbox/utilities/jinja2.py

@@ -49,11 +49,27 @@ class DataFileLoader(BaseLoader):
 # Utility functions
 #
 
-def render_jinja2(template_code, context, environment_params=None):
+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)
-    return environment.from_string(source=template_code).render(**context)
+
+    if data_file:
+        template = environment.get_template(data_file.path)
+    else:
+        template = environment.from_string(source=template_code)
+    return template.render(**context)