mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Create RenderMixin, and unify template_code rendering and exporting
This commit is contained in:
parent
6a966ee6c1
commit
fa8343b7cb
@ -4,7 +4,6 @@ from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
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
|
||||
@ -2293,10 +2292,7 @@ class DeviceRenderConfigView(generic.ObjectView):
|
||||
# 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(), {
|
||||
|
@ -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():
|
||||
|
@ -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=_('Attributes'))
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DataSource.objects.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):
|
||||
|
@ -246,7 +246,8 @@ 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:
|
||||
@ -631,8 +632,10 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'environment_params', 'tags', name=_('Config Template')),
|
||||
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('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2b1 on 2025-04-03 01:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0125_exporttemplate_file_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exporttemplate',
|
||||
name='environment_params',
|
||||
field=models.JSONField(blank=True, default=dict, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2b1 on 2025-04-04 00:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0126_exporttemplate_environment_params'),
|
||||
]
|
||||
|
||||
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),
|
||||
),
|
||||
]
|
@ -4,16 +4,13 @@ from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jinja2.loaders import BaseLoader
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from netbox.config import get_config
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
|
||||
from netbox.models.features import (
|
||||
CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, RenderMixin)
|
||||
from netbox.registry import registry
|
||||
from utilities.data import deepmerge
|
||||
from utilities.jinja2 import DataFileLoader
|
||||
|
||||
__all__ = (
|
||||
'ConfigContext',
|
||||
@ -210,7 +207,8 @@ class ConfigContextModel(models.Model):
|
||||
# Config templates
|
||||
#
|
||||
|
||||
class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
class ConfigTemplate(
|
||||
SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel, RenderMixin):
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100
|
||||
@ -220,20 +218,6 @@ class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, Ta
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
template_code = models.TextField(
|
||||
verbose_name=_('template code'),
|
||||
help_text=_('Jinja2 template code.')
|
||||
)
|
||||
environment_params = models.JSONField(
|
||||
verbose_name=_('environment parameters'),
|
||||
blank=True,
|
||||
null=True,
|
||||
default=dict,
|
||||
help_text=_(
|
||||
'Any <a href="https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment">additional parameters</a>'
|
||||
' to pass when constructing the Jinja2 environment.'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
@ -253,13 +237,8 @@ class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, Ta
|
||||
self.template_code = self.data_file.data_as_string
|
||||
sync_data.alters_data = True
|
||||
|
||||
def render(self, context=None):
|
||||
"""
|
||||
Render the contents of the template.
|
||||
"""
|
||||
def get_context(self, context=None, queryset=None):
|
||||
_context = dict()
|
||||
|
||||
# Populate the default template context with NetBox model classes, namespaced by app
|
||||
for app, model_names in registry['models'].items():
|
||||
_context.setdefault(app, {})
|
||||
for model_name in model_names:
|
||||
@ -273,33 +252,4 @@ class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, Ta
|
||||
if context is not None:
|
||||
_context.update(context)
|
||||
|
||||
# Initialize the Jinja2 environment and instantiate the Template
|
||||
environment = self._get_environment()
|
||||
if self.data_file:
|
||||
template = environment.get_template(self.data_file.path)
|
||||
else:
|
||||
template = environment.from_string(self.template_code)
|
||||
output = template.render(**_context)
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
return output.replace('\r\n', '\n')
|
||||
|
||||
def _get_environment(self):
|
||||
"""
|
||||
Instantiate and return a Jinja2 environment suitable for rendering the ConfigTemplate.
|
||||
"""
|
||||
# Initialize the template loader & cache the base template code (if applicable)
|
||||
if self.data_file:
|
||||
loader = DataFileLoader(data_source=self.data_source)
|
||||
loader.cache_templates({
|
||||
self.data_file.path: self.template_code
|
||||
})
|
||||
else:
|
||||
loader = BaseLoader()
|
||||
|
||||
# Initialize the environment
|
||||
env_params = self.environment_params or {}
|
||||
environment = SandboxedEnvironment(loader=loader, **env_params)
|
||||
environment.filters.update(get_config().JINJA2_FILTERS)
|
||||
|
||||
return environment
|
||||
return _context
|
||||
|
@ -6,7 +6,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -16,12 +15,12 @@ from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.constants import *
|
||||
from extras.utils import filename_from_model, image_upload
|
||||
from extras.utils import image_upload
|
||||
from netbox.config import get_config
|
||||
from netbox.events import get_event_type_choices
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, RenderMixin
|
||||
)
|
||||
from utilities.html import clean_html
|
||||
from utilities.jinja2 import render_jinja2
|
||||
@ -382,7 +381,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
}
|
||||
|
||||
|
||||
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderMixin):
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='export_templates',
|
||||
@ -397,34 +396,6 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
template_code = models.TextField(
|
||||
help_text=_(
|
||||
"Jinja2 template code. The list of objects being exported is passed as a context variable named "
|
||||
"<code>queryset</code>."
|
||||
)
|
||||
)
|
||||
mime_type = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('MIME type'),
|
||||
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
|
||||
)
|
||||
file_name = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
help_text=_('Filename to give to the rendered export file')
|
||||
)
|
||||
file_extension = models.CharField(
|
||||
verbose_name=_('file extension'),
|
||||
max_length=15,
|
||||
blank=True,
|
||||
help_text=_('Extension to append to the rendered filename')
|
||||
)
|
||||
as_attachment = models.BooleanField(
|
||||
verbose_name=_('as attachment'),
|
||||
default=True,
|
||||
help_text=_("Download file as attachment")
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'object_types', 'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment',
|
||||
@ -460,37 +431,11 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
||||
self.template_code = self.data_file.data_as_string
|
||||
sync_data.alters_data = True
|
||||
|
||||
def render(self, queryset):
|
||||
"""
|
||||
Render the contents of the template.
|
||||
"""
|
||||
context = {
|
||||
def get_context(self, context=None, queryset=None):
|
||||
export_context = {
|
||||
'queryset': queryset
|
||||
}
|
||||
output = render_jinja2(self.template_code, context)
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
return output
|
||||
|
||||
def render_to_response(self, queryset):
|
||||
"""
|
||||
Render the template to an HTTP response, delivered as a named file attachment
|
||||
"""
|
||||
output = self.render(queryset)
|
||||
mime_type = 'text/plain; charset=utf-8' if not self.mime_type else self.mime_type
|
||||
|
||||
# Build the response
|
||||
response = HttpResponse(output, content_type=mime_type)
|
||||
|
||||
if self.as_attachment:
|
||||
extension = f'.{self.file_extension}' if self.file_extension else ''
|
||||
filename = self.file_name or filename_from_model(queryset.model)
|
||||
full_filename = f'{filename}{extension}'
|
||||
response['Content-Disposition'] = f'attachment; filename="{full_filename}"'
|
||||
|
||||
return response
|
||||
return export_context
|
||||
|
||||
|
||||
class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
@ -550,12 +550,17 @@ class ConfigTemplateTable(NetBoxTable):
|
||||
url_params={'config_template_id': 'pk'},
|
||||
verbose_name=_('Virtual Machines')
|
||||
)
|
||||
as_attachment = columns.BooleanColumn(
|
||||
verbose_name=_('As Attachment'),
|
||||
false_mark=None
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ConfigTemplate
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'description', 'data_source', 'data_file', 'data_synced', 'role_count',
|
||||
'platform_count', 'device_count', 'vm_count', 'created', 'last_updated', 'tags',
|
||||
'platform_count', 'device_count', 'vm_count', 'created', 'last_updated', 'tags', 'as_attachment',
|
||||
'mime_type', 'file_name', 'file_extension',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'description', 'is_synced', 'device_count', 'vm_count',
|
||||
|
@ -616,7 +616,7 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
|
||||
class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
filterset = ExportTemplateFilterSet
|
||||
ignore_fields = ('template_code', 'data_path')
|
||||
ignore_fields = ('template_code', 'environment_params', 'data_path')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -22,6 +22,17 @@ def filename_from_model(model: models.Model) -> str:
|
||||
return f'netbox_{base}'
|
||||
|
||||
|
||||
def filename_from_object(context: dict) -> str:
|
||||
"""Standardises how we generate filenames from model class for exports"""
|
||||
if 'device' in context:
|
||||
base = f"{context['device'].name or 'config'}"
|
||||
elif 'virtualmachine' in context:
|
||||
base = f"{context['virtualmachine'].name or 'config'}"
|
||||
else:
|
||||
base = 'config'
|
||||
return base
|
||||
|
||||
|
||||
def is_taggable(obj):
|
||||
"""
|
||||
Return True if the instance can have Tags assigned to it; False otherwise.
|
||||
|
@ -45,7 +45,7 @@ class ExportTemplatesMixin:
|
||||
if et is None:
|
||||
raise Http404
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return et.render_to_response(queryset)
|
||||
return et.render_to_response(queryset=queryset)
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
@ -14,10 +15,11 @@ from core.choices import JobStatusChoices, ObjectChangeActionChoices
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
|
||||
from extras.utils import is_taggable
|
||||
from extras.utils import is_taggable, filename_from_model, filename_from_object
|
||||
from netbox.config import get_config
|
||||
from netbox.registry import registry
|
||||
from netbox.signals import post_clean
|
||||
from utilities.jinja2 import render_jinja2
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
from utilities.serialization import serialize_object
|
||||
from utilities.views import register_model_view
|
||||
@ -37,6 +39,7 @@ __all__ = (
|
||||
'JournalingMixin',
|
||||
'NotificationsMixin',
|
||||
'SyncedDataMixin',
|
||||
'RenderMixin',
|
||||
'TagsMixin',
|
||||
'register_models',
|
||||
)
|
||||
@ -337,6 +340,87 @@ class ExportTemplatesMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class RenderMixin(models.Model):
|
||||
"""
|
||||
Enables support for rendering templates.
|
||||
"""
|
||||
template_code = models.TextField(
|
||||
verbose_name=_('template code'),
|
||||
help_text=_('Jinja2 template code.')
|
||||
)
|
||||
environment_params = models.JSONField(
|
||||
verbose_name=_('environment parameters'),
|
||||
blank=True,
|
||||
null=True,
|
||||
default=dict,
|
||||
help_text=_(
|
||||
'Any <a href="https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment">additional parameters</a>'
|
||||
' to pass when constructing the Jinja2 environment.'
|
||||
)
|
||||
)
|
||||
mime_type = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('MIME type'),
|
||||
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
|
||||
)
|
||||
file_name = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
help_text=_('Filename to give to the rendered export file')
|
||||
)
|
||||
file_extension = models.CharField(
|
||||
verbose_name=_('file extension'),
|
||||
max_length=15,
|
||||
blank=True,
|
||||
help_text=_('Extension to append to the rendered filename')
|
||||
)
|
||||
as_attachment = models.BooleanField(
|
||||
verbose_name=_('as attachment'),
|
||||
default=True,
|
||||
help_text=_("Download file as attachment")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_context(self, context=None, queryset=None):
|
||||
raise NotImplementedError(_("{class_name} must implement a get_context() method.").format(
|
||||
class_name=self.__class__
|
||||
))
|
||||
|
||||
def render(self, context=None, queryset=None):
|
||||
"""
|
||||
Render the template with the provided context. The context is passed to the Jinja2 environment as a dictionary.
|
||||
"""
|
||||
context = self.get_context(context=context, queryset=queryset)
|
||||
env_params = self.environment_params or {}
|
||||
output = render_jinja2(self.template_code, context, env_params)
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
return output
|
||||
|
||||
def render_to_response(self, context=None, queryset=None):
|
||||
output = self.render(context=context, queryset=queryset)
|
||||
mime_type = self.mime_type or 'text/plain; charset=utf-8'
|
||||
|
||||
# Build the response
|
||||
response = HttpResponse(output, content_type=mime_type)
|
||||
|
||||
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)
|
||||
elif context:
|
||||
filename = self.file_name or filename_from_object(context)
|
||||
full_filename = f'{filename}{extension}'
|
||||
response['Content-Disposition'] = f'attachment; filename="{full_filename}"'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ImageAttachmentsMixin(models.Model):
|
||||
"""
|
||||
Enables the assignments of ImageAttachments.
|
||||
|
@ -107,7 +107,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
request: The current request
|
||||
"""
|
||||
try:
|
||||
return template.render_to_response(self.queryset)
|
||||
return template.render_to_response(queryset=self.queryset)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
|
@ -17,6 +17,22 @@
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "MIME Type" %}</th>
|
||||
<td>{{ object.mime_type|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "File Name" %}</th>
|
||||
<td>{{ object.file_name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "File Extension" %}</th>
|
||||
<td>{{ object.file_extension|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Attachment" %}</th>
|
||||
<td>{% checkmark object.as_attachment %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Data Source" %}</th>
|
||||
<td>
|
||||
|
@ -86,6 +86,12 @@
|
||||
<pre>{{ object.template_code }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Environment Parameters" %}</h2>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.environment_params }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,10 +48,10 @@ class DataFileLoader(BaseLoader):
|
||||
# Utility functions
|
||||
#
|
||||
|
||||
def render_jinja2(template_code, context):
|
||||
def render_jinja2(template_code, context, environment_params={}):
|
||||
"""
|
||||
Render a Jinja2 template with the provided context. Return the rendered content.
|
||||
"""
|
||||
environment = SandboxedEnvironment()
|
||||
environment = SandboxedEnvironment(**environment_params)
|
||||
environment.filters.update(get_config().JINJA2_FILTERS)
|
||||
return environment.from_string(source=template_code).render(**context)
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch, Sum
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -431,10 +430,7 @@ class VirtualMachineRenderConfigView(generic.ObjectView):
|
||||
# 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(), {
|
||||
|
Loading…
Reference in New Issue
Block a user