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.