From 36fb044de5a2d7ca91b72eefa224dd2893e017ac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 8 Apr 2025 10:03:26 -0400 Subject: [PATCH] Misc cleanup --- docs/models/extras/configtemplate.md | 10 ++- docs/models/extras/exporttemplate.md | 4 +- netbox/extras/forms/model_forms.py | 11 +-- netbox/extras/graphql/filters.py | 2 + netbox/extras/models/configs.py | 6 +- netbox/extras/models/mixins.py | 21 +++--- netbox/extras/models/models.py | 10 ++- netbox/extras/tables/tables.py | 12 ++-- netbox/extras/tests/test_api.py | 7 +- netbox/extras/tests/test_filtersets.py | 99 +++++++++++++++++++------- netbox/extras/tests/test_views.py | 24 +++++-- netbox/utilities/jinja2.py | 3 +- 12 files changed, 140 insertions(+), 69 deletions(-) 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)