diff --git a/docs/models/extras/configtemplate.md b/docs/models/extras/configtemplate.md
index 262e8422a..6b245e5e9 100644
--- a/docs/models/extras/configtemplate.md
+++ b/docs/models/extras/configtemplate.md
@@ -26,18 +26,24 @@ A dictionary of any additional parameters to pass when instantiating the [Jinja2
### MIME Type
+!!! info "This field was introduced in NetBox v4.3."
+
The MIME type to indicate in the response when rendering the configuration template (optional). Defaults to `text/plain`.
### File Name
-The file name to give to the rendered export file (optional).
-
!!! info "This field was introduced in NetBox v4.3."
+The file name to give to the rendered export file (optional).
+
### File Extension
+!!! info "This field was introduced in NetBox v4.3."
+
The file extension to append to the file name in the response (optional).
### As Attachment
+!!! info "This field was introduced in NetBox v4.3."
+
If selected, the rendered content will be returned as a file attachment, rather than displayed directly in-browser (where supported).
\ No newline at end of file
diff --git a/docs/models/extras/exporttemplate.md b/docs/models/extras/exporttemplate.md
index 31a15dc1c..86e1ae04a 100644
--- a/docs/models/extras/exporttemplate.md
+++ b/docs/models/extras/exporttemplate.md
@@ -22,6 +22,8 @@ Jinja2 template code for rendering the exported data.
### Environment Parameters
+!!! info "This field was introduced in NetBox v4.3."
+
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.
### MIME Type
@@ -32,8 +34,6 @@ The MIME type to indicate in the response when rendering the export template (op
The file name to give to the rendered export file (optional).
-!!! info "This field was introduced in NetBox v4.3."
-
### File Extension
The file extension to append to the file name in the response (optional).
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 4d7a0c4c7..594b7d9d0 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -247,7 +247,8 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
FieldSet(
- 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')),
+ 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
+ ),
)
class Meta:
@@ -632,11 +633,11 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
)
fieldsets = (
- FieldSet('name', 'description', 'tags', name=_('Config Template')),
- FieldSet('template_code', name=_('Content')),
- FieldSet(
- 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')),
+ FieldSet('name', 'description', 'tags', 'template_code', name=_('Config Template')),
FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
+ FieldSet(
+ 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
+ ),
)
class Meta:
diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py
index 1e48c6c49..b8db143e4 100644
--- a/netbox/extras/graphql/filters.py
+++ b/netbox/extras/graphql/filters.py
@@ -105,6 +105,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
strawberry_django.filter_field()
)
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
+ file_name: FilterLookup[str] | None = strawberry_django.filter_field()
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@@ -200,6 +201,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
strawberry_django.filter_field()
)
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
+ file_name: FilterLookup[str] | None = strawberry_django.filter_field()
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py
index 8b47fced5..204cc04f9 100644
--- a/netbox/extras/models/configs.py
+++ b/netbox/extras/models/configs.py
@@ -5,8 +5,8 @@ from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
-from extras.querysets import ConfigContextQuerySet
from extras.models.mixins import RenderTemplateMixin
+from extras.querysets import ConfigContextQuerySet
from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from netbox.registry import registry
@@ -208,7 +208,7 @@ class ConfigContextModel(models.Model):
#
class ConfigTemplate(
- SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel, RenderTemplateMixin
+ RenderTemplateMixin, SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel
):
name = models.CharField(
verbose_name=_('name'),
@@ -249,7 +249,7 @@ class ConfigTemplate(
except LookupError:
pass
- # Add the provided context data, if any
+ # Apply the provided context data, if any
if context is not None:
_context.update(context)
diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py
index fa3c64d2a..3a7273f93 100644
--- a/netbox/extras/models/mixins.py
+++ b/netbox/extras/models/mixins.py
@@ -91,16 +91,14 @@ class RenderTemplateMixin(models.Model):
null=True,
default=dict,
help_text=_(
- 'Any additional parameters to pass when constructing the Jinja2 environment.'
- ).format(url='https://jinja.palletsprojects.com/en/stable/api/#jinja2.Environment')
+ 'Any additional parameters to pass when constructing the Jinja environment'
+ ).format(url='https://jinja.palletsprojects.com/en/stable/api/#jinja2.Environment')
)
mime_type = models.CharField(
max_length=50,
blank=True,
verbose_name=_('MIME type'),
- help_text=_('Defaults to {default}
').format(
- default=DEFAULT_MIME_TYPE
- ),
+ help_text=_('Defaults to {default}
').format(default=DEFAULT_MIME_TYPE),
)
file_name = models.CharField(
max_length=200,
@@ -149,13 +147,14 @@ class RenderTemplateMixin(models.Model):
if self.as_attachment:
extension = f'.{self.file_extension}' if self.file_extension else ''
- if queryset:
- filename = self.file_name or filename_from_model(queryset.model)
+ if self.file_name:
+ filename = self.file_name
+ elif queryset:
+ filename = filename_from_model(queryset.model)
elif context:
- filename = self.file_name or filename_from_object(context)
+ filename = filename_from_object(context)
else:
- filename = self.file_name or "template"
- full_filename = f'{filename}{extension}'
- response['Content-Disposition'] = f'attachment; filename="{full_filename}"'
+ filename = "output"
+ response['Content-Disposition'] = f'attachment; filename="{filename}{extension}"'
return response
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 3aed1c4c1..76fad1082 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -433,10 +433,16 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
sync_data.alters_data = True
def get_context(self, context=None, queryset=None):
- return {
- 'queryset': queryset
+ _context = {
+ 'queryset': queryset,
}
+ # Apply the provided context data, if any
+ if context is not None:
+ _context.update(context)
+
+ return _context
+
class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
"""
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index c01972d5f..60b207058 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -184,13 +184,13 @@ class ExportTemplateTable(NetBoxTable):
verbose_name=_('Object Types'),
)
mime_type = tables.Column(
- verbose_name=_('MIME type')
+ verbose_name=_('MIME Type')
)
file_name = tables.Column(
- verbose_name=_('File name'),
+ verbose_name=_('File Name'),
)
file_extension = tables.Column(
- verbose_name=_('File extension'),
+ verbose_name=_('File Extension'),
)
as_attachment = columns.BooleanColumn(
verbose_name=_('As Attachment'),
@@ -537,13 +537,13 @@ class ConfigTemplateTable(NetBoxTable):
verbose_name=_('Synced')
)
mime_type = tables.Column(
- verbose_name=_('MIME type')
+ verbose_name=_('MIME Type')
)
file_name = tables.Column(
- verbose_name=_('File name'),
+ verbose_name=_('File Name'),
)
file_extension = tables.Column(
- verbose_name=_('File extension'),
+ verbose_name=_('File Extension'),
)
as_attachment = columns.BooleanColumn(
verbose_name=_('As Attachment'),
diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py
index 4682ce6e2..6e3fb37fc 100644
--- a/netbox/extras/tests/test_api.py
+++ b/netbox/extras/tests/test_api.py
@@ -755,6 +755,10 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
{
'name': 'Config Template 4',
'template_code': 'Foo: {{ foo }}',
+ 'mime_type': 'text/plain',
+ 'file_name': 'output4',
+ 'file_extension': 'txt',
+ 'as_attachment': True,
},
{
'name': 'Config Template 5',
@@ -763,7 +767,6 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
{
'name': 'Config Template 6',
'template_code': 'Baz: {{ baz }}',
- 'file_name': 'test_config_template_6',
},
]
bulk_update_data = {
@@ -780,8 +783,6 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
ConfigTemplate(
name='Config Template 2',
template_code='Bar: {{ bar }}',
- file_name='config_template_2',
- file_extension='test',
),
ConfigTemplate(
name='Config Template 3',
diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py
index ce4f16c44..987dfe0ff 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -623,12 +623,32 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
export_templates = (
- ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
ExportTemplate(
- name='Export Template 2', template_code='TESTING', description='foobar2',
- file_name='export_template_2', file_extension='nagios',
+ name='Export Template 1',
+ template_code='TESTING',
+ description='foobar1',
+ mime_type='text/foo',
+ file_name='foo',
+ file_extension='foo',
+ as_attachment=True,
+ ),
+ ExportTemplate(
+ name='Export Template 2',
+ template_code='TESTING',
+ description='foobar2',
+ mime_type='text/bar',
+ file_name='bar',
+ file_extension='bar',
+ as_attachment=True,
+ ),
+ ExportTemplate(
+ name='Export Template 3',
+ template_code='TESTING',
+ mime_type='text/baz',
+ file_name='baz',
+ file_extension='baz',
+ as_attachment=False,
),
- ExportTemplate(name='Export Template 3', template_code='TESTING', file_name='export_filename'),
)
ExportTemplate.objects.bulk_create(export_templates)
for i, et in enumerate(export_templates):
@@ -638,9 +658,6 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
- params = {'q': 'export_filename'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
def test_name(self):
params = {'name': ['Export Template 1', 'Export Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -655,19 +672,21 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_mime_type(self):
+ params = {'mime_type': ['text/foo', 'text/bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_file_name(self):
- params = {'file_name': ['export_filename']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'file_name': ['foo', 'bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_file_extension(self):
- params = {'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'file_extension': ['foo', 'bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'file_name': ['export_template_2'], 'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- params = {'file_name': 'export_filename', 'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+ def test_as_attachment(self):
+ params = {'as_attachment': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1088,12 +1107,32 @@ class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
config_templates = (
- ConfigTemplate(name='Config Template 1', template_code='TESTING', description='foobar1'),
ConfigTemplate(
- name='Config Template 2', template_code='TESTING', description='foobar2',
- file_name='config_template_2', file_extension='nagios',
+ name='Config Template 1',
+ template_code='TESTING',
+ description='foobar1',
+ mime_type='text/foo',
+ file_name='foo',
+ file_extension='foo',
+ as_attachment=True,
+ ),
+ ConfigTemplate(
+ name='Config Template 2',
+ template_code='TESTING',
+ description='foobar2',
+ mime_type='text/bar',
+ file_name='bar',
+ file_extension='bar',
+ as_attachment=True,
+ ),
+ ConfigTemplate(
+ name='Config Template 3',
+ template_code='TESTING',
+ mime_type='text/baz',
+ file_name='baz',
+ file_extension='baz',
+ as_attachment=False,
),
- ConfigTemplate(name='Config Template 3', template_code='TESTING', file_name='export_filename'),
)
ConfigTemplate.objects.bulk_create(config_templates)
@@ -1109,15 +1148,21 @@ class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_mime_type(self):
+ params = {'mime_type': ['text/foo', 'text/bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_file_name(self):
+ params = {'file_name': ['foo', 'bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_file_extension(self):
- params = {'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'file_extension': ['foo', 'bar']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'file_name': ['config_template_2'], 'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- params = {'file_name': 'export_filename', 'file_extension': ['nagios']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+ def test_as_attachment(self):
+ params = {'as_attachment': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index 283b60168..6378b29b8 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -544,11 +544,20 @@ class ConfigTemplateTestCase(
ENVIRONMENT_PARAMS = """{"trim_blocks": true}"""
config_templates = (
- ConfigTemplate(name='Config Template 1', template_code=TEMPLATE_CODE),
ConfigTemplate(
- name='Config Template 2', template_code=TEMPLATE_CODE, environment_params={"trim_blocks": True}
+ name='Config Template 1',
+ template_code=TEMPLATE_CODE)
+ ,
+ ConfigTemplate(
+ name='Config Template 2',
+ template_code=TEMPLATE_CODE,
+ environment_params={"trim_blocks": True},
+ ),
+ ConfigTemplate(
+ name='Config Template 3',
+ template_code=TEMPLATE_CODE,
+ file_name='config_template_3',
),
- ConfigTemplate(name='Config Template 3', template_code=TEMPLATE_CODE, file_name='config_template_3'),
)
ConfigTemplate.objects.bulk_create(config_templates)
@@ -561,15 +570,16 @@ class ConfigTemplateTestCase(
}
cls.csv_update_data = (
- "id,name,template_code,file_name",
- f"{config_templates[0].pk},Config Template 7,{TEMPLATE_CODE},",
- f"{config_templates[1].pk},Config Template 8,{TEMPLATE_CODE},config_8",
- f"{config_templates[2].pk},Config Template 9,{TEMPLATE_CODE},",
+ "id,name",
+ f"{config_templates[0].pk},Config Template 7",
+ f"{config_templates[1].pk},Config Template 8",
+ f"{config_templates[2].pk},Config Template 9",
)
cls.bulk_edit_data = {
'description': 'New description',
'mime_type': 'text/html',
+ 'file_name': 'output',
'file_extension': 'html',
'as_attachment': True,
}
diff --git a/netbox/utilities/jinja2.py b/netbox/utilities/jinja2.py
index b2bd2bd5a..37b3b2dfb 100644
--- a/netbox/utilities/jinja2.py
+++ b/netbox/utilities/jinja2.py
@@ -49,10 +49,11 @@ class DataFileLoader(BaseLoader):
# Utility functions
#
-def render_jinja2(template_code, context, environment_params={}):
+def render_jinja2(template_code, context, environment_params=None):
"""
Render a Jinja2 template with the provided context. Return the rendered content.
"""
+ environment_params = environment_params or {}
environment = SandboxedEnvironment(**environment_params)
environment.filters.update(get_config().JINJA2_FILTERS)
return environment.from_string(source=template_code).render(**context)