Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #17653 Add function to trim whitespaces in export templates via jinja environment settings #19078

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions docs/models/extras/configtemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ See the [configuration rendering documentation](../../features/configuration-ren

A unique human-friendly name.

### Weight

A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.

### Data File

Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
Expand All @@ -27,3 +23,27 @@ Jinja2 template code, if being defined locally rather than replicated from a dat
### Environment Parameters

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

!!! 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

!!! 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).
8 changes: 6 additions & 2 deletions docs/models/extras/exporttemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Template code may optionally be sourced from a remote [data file](../core/datafi

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

The MIME type to indicate in the response when rendering the export template (optional). Defaults to `text/plain`.
Expand All @@ -28,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).
Expand Down
6 changes: 1 addition & 5 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.db import transaction
from django.db.models import Prefetch
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
Expand Down Expand Up @@ -2293,10 +2292,7 @@ def get(self, request, **kwargs):
# If a direct export has been requested, return the rendered template content as a
# downloadable file.
if request.GET.get('export'):
content = context['rendered_config'] or context['error_message']
response = HttpResponse(content, content_type='text')
filename = f"{instance.name or 'config'}.txt"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
response = context['config_template'].render_to_response(context=context['context_data'])
return response

return render(request, self.get_template_name(), {
Expand Down
3 changes: 2 additions & 1 deletion netbox/extras/api/serializers_/configtemplates.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Meta:
model = ConfigTemplate
fields = [
'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code',
'data_source', 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file',
'data_synced', 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
6 changes: 3 additions & 3 deletions netbox/extras/api/serializers_/exporttemplates.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ExportTemplate
fields = [
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
'file_name', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced',
'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'environment_params',
'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source',
'data_path', 'data_file', 'data_synced', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
3 changes: 3 additions & 0 deletions netbox/extras/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])

# Template Export
DEFAULT_MIME_TYPE = 'text/plain; charset=utf-8'

# Webhooks
HTTP_CONTENT_TYPE_JSON = 'application/json'

Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,10 @@ class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet):

class Meta:
model = ConfigTemplate
fields = ('id', 'name', 'description', 'auto_sync_enabled', 'data_synced')
fields = (
'id', 'name', 'description', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
'auto_sync_enabled', 'data_synced'
)

def search(self, queryset, name, value):
if not value.strip():
Expand Down
21 changes: 20 additions & 1 deletion netbox/extras/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,27 @@ class ConfigTemplateBulkEditForm(BulkEditForm):
max_length=200,
required=False
)
mime_type = forms.CharField(
label=_('MIME type'),
max_length=50,
required=False
)
file_name = forms.CharField(
label=_('File name'),
required=False
)
file_extension = forms.CharField(
label=_('File extension'),
max_length=15,
required=False
)
as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False,
widget=BulkEditNullBooleanSelect()
)

nullable_fields = ('description',)
nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension')


class JournalEntryBulkEditForm(BulkEditForm):
Expand Down
7 changes: 4 additions & 3 deletions netbox/extras/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ class ExportTemplateImportForm(CSVModelForm):
class Meta:
model = ExportTemplate
fields = (
'name', 'object_types', 'description', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
'template_code',
'name', 'object_types', 'description', 'environment_params', 'mime_type', 'file_name', 'file_extension',
'as_attachment', 'template_code',
)


Expand All @@ -154,7 +154,8 @@ class ConfigTemplateImportForm(CSVModelForm):
class Meta:
model = ConfigTemplate
fields = (
'name', 'description', 'environment_params', 'template_code', 'tags',
'name', 'description', 'template_code', 'environment_params', 'mime_type', 'file_name', 'file_extension',
'as_attachment', 'tags',
)


Expand Down
24 changes: 22 additions & 2 deletions netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):

class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('q', 'filter_id', 'object_type_id'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
FieldSet('object_type_id', 'mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Attributes')),
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
Expand Down Expand Up @@ -410,6 +410,7 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering'))
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
Expand All @@ -425,6 +426,25 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
}
)
tag = TagFilterField(ConfigTemplate)
mime_type = forms.CharField(
required=False,
label=_('MIME type')
)
file_name = forms.CharField(
label=_('File name'),
required=False
)
file_extension = forms.CharField(
label=_('File extension'),
required=False
)
as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)


class LocalConfigContextFilterForm(forms.Form):
Expand Down
10 changes: 7 additions & 3 deletions netbox/extras/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
fieldsets = (
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', 'as_attachment', name=_('Rendering')),
FieldSet(
'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
),
)

class Meta:
Expand Down Expand Up @@ -631,9 +633,11 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
)

fieldsets = (
FieldSet('name', 'description', 'environment_params', 'tags', name=_('Config Template')),
FieldSet('template_code', name=_('Content')),
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:
Expand Down
8 changes: 8 additions & 0 deletions netbox/extras/graphql/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
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()


@strawberry_django.filter(models.CustomField, lookups=True)
Expand Down Expand Up @@ -193,7 +197,11 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
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()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.2b1 on 2025-04-04 20:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('extras', '0125_exporttemplate_file_name'),
]

operations = [
migrations.AddField(
model_name='configtemplate',
name='as_attachment',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='configtemplate',
name='file_extension',
field=models.CharField(blank=True, max_length=15),
),
migrations.AddField(
model_name='configtemplate',
name='file_name',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='configtemplate',
name='mime_type',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='exporttemplate',
name='environment_params',
field=models.JSONField(blank=True, default=dict, null=True),
),
]
Loading