diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py
index 65e7ed699..3e7a9b3f2 100644
--- a/netbox/extras/models/configs.py
+++ b/netbox/extras/models/configs.py
@@ -6,9 +6,9 @@ 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 netbox.models import ChangeLoggedModel
-from netbox.models.features import (
- CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, RenderMixin)
+from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from netbox.registry import registry
from utilities.data import deepmerge
@@ -208,7 +208,7 @@ class ConfigContextModel(models.Model):
#
class ConfigTemplate(
- SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel, RenderMixin):
+ SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel, RenderTemplateMixin):
name = models.CharField(
verbose_name=_('name'),
max_length=100
diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py
index f22b32004..fa3c64d2a 100644
--- a/netbox/extras/models/mixins.py
+++ b/netbox/extras/models/mixins.py
@@ -3,9 +3,18 @@ import importlib.util
import os
import sys
from django.core.files.storage import storages
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from django.http import HttpResponse
+
+from extras.constants import DEFAULT_MIME_TYPE
+from extras.utils import filename_from_model, filename_from_object
+from utilities.jinja2 import render_jinja2
+
__all__ = (
'PythonModuleMixin',
+ 'RenderTemplateMixin',
)
@@ -66,3 +75,87 @@ class PythonModuleMixin:
loader.exec_module(module)
return module
+
+
+class RenderTemplateMixin(models.Model):
+ """
+ Enables support for rendering templates.
+ """
+ template_code = models.TextField(
+ verbose_name=_('template code'),
+ help_text=_('Jinja template code.')
+ )
+ environment_params = models.JSONField(
+ verbose_name=_('environment parameters'),
+ blank=True,
+ 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')
+ )
+ mime_type = models.CharField(
+ max_length=50,
+ blank=True,
+ verbose_name=_('MIME type'),
+ help_text=_('Defaults to {default}
').format(
+ default=DEFAULT_MIME_TYPE
+ ),
+ )
+ 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 DEFAULT_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 ''
+ if queryset:
+ filename = self.file_name or filename_from_model(queryset.model)
+ elif context:
+ filename = self.file_name or filename_from_object(context)
+ else:
+ filename = self.file_name or "template"
+ full_filename = f'{filename}{extension}'
+ response['Content-Disposition'] = f'attachment; filename="{full_filename}"'
+
+ return response
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 8619445cf..16438e71d 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -16,11 +16,12 @@ from extras.choices import *
from extras.conditions import ConditionSet
from extras.constants import *
from extras.utils import image_upload
+from extras.models.mixins import RenderTemplateMixin
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, RenderMixin
+ CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
)
from utilities.html import clean_html
from utilities.jinja2 import render_jinja2
@@ -381,7 +382,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
}
-class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderMixin):
+class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderTemplateMixin):
object_types = models.ManyToManyField(
to='core.ObjectType',
related_name='export_templates',
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index bb5d40419..d14fdb17f 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -6,7 +6,6 @@ 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,12 +13,11 @@ from taggit.managers import TaggableManager
from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.models import ObjectType
from extras.choices import *
-from extras.constants import CUSTOMFIELD_EMPTY_VALUES, DEFAULT_MIME_TYPE
-from extras.utils import is_taggable, filename_from_model, filename_from_object
+from extras.constants import CUSTOMFIELD_EMPTY_VALUES
+from extras.utils import is_taggable
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
@@ -39,7 +37,6 @@ __all__ = (
'JournalingMixin',
'NotificationsMixin',
'SyncedDataMixin',
- 'RenderMixin',
'TagsMixin',
'register_models',
)
@@ -340,89 +337,6 @@ 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 additional parameters'
- ' to pass when constructing the Jinja2 environment.'
- )
- )
- mime_type = models.CharField(
- max_length=50,
- blank=True,
- verbose_name=_('MIME type'),
- help_text=_('Defaults to {default_mime_type}').format(
- default_mime_type=DEFAULT_MIME_TYPE
- ),
- )
- 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 DEFAULT_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 ''
- 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.