diff --git a/CHANGELOG.md b/CHANGELOG.md index bd36ecc9d..4fec2d5ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ v2.5.10 (FUTURE) +## Enhancements + +* [#3052](https://github.com/digitalocean/netbox/issues/3052) - Add Jinja2 support for export templates + ## Bug Fixes * [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 981d649fb..cca783bc6 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -55,10 +55,17 @@ class RenderedGraphSerializer(serializers.ModelSerializer): # class ExportTemplateSerializer(ValidatedModelSerializer): + template_language = ChoiceField( + choices=TEMPLATE_LANGUAGE_CHOICES, + default=TEMPLATE_LANGUAGE_JINJA2 + ) class Meta: model = ExportTemplate - fields = ['id', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension'] + fields = [ + 'id', 'content_type', 'name', 'description', 'template_language', 'template_code', 'mime_type', + 'file_extension', + ] # diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index ec30c6a66..30ef826e8 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -23,6 +23,7 @@ from . import serializers class ExtrasFieldChoicesViewSet(FieldChoicesViewSet): fields = ( + (ExportTemplate, ['template_language']), (Graph, ['type']), (ObjectChange, ['action']), ) diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 51fc398f7..13c15cbba 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -56,6 +56,14 @@ EXPORTTEMPLATE_MODELS = [ 'cluster', 'virtualmachine', # Virtualization ] +# ExportTemplate language choices +TEMPLATE_LANGUAGE_DJANGO = 10 +TEMPLATE_LANGUAGE_JINJA2 = 20 +TEMPLATE_LANGUAGE_CHOICES = ( + (TEMPLATE_LANGUAGE_DJANGO, 'Django'), + (TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'), +) + # Topology map types TOPOLOGYMAP_TYPE_NETWORK = 1 TOPOLOGYMAP_TYPE_CONSOLE = 2 diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index d0a801b48..d5457a5a6 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -82,7 +82,7 @@ class ExportTemplateFilter(django_filters.FilterSet): class Meta: model = ExportTemplate - fields = ['content_type', 'name'] + fields = ['content_type', 'name', 'template_language'] class TagFilter(django_filters.FilterSet): diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index b48482c93..54eee0c5c 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -4,7 +4,6 @@ from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from mptt.forms import TreeNodeMultipleChoiceField from taggit.forms import TagField from taggit.models import Tag @@ -12,7 +11,7 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, - FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, + FilterChoiceField, LaxURLField, JSONField, SlugField, ) from .constants import ( CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, diff --git a/netbox/extras/migrations/0018_exporttemplate_add_jinja2.py b/netbox/extras/migrations/0018_exporttemplate_add_jinja2.py new file mode 100644 index 000000000..1177ac2fb --- /dev/null +++ b/netbox/extras/migrations/0018_exporttemplate_add_jinja2.py @@ -0,0 +1,27 @@ +# Generated by Django 2.1.7 on 2019-04-08 14:49 + +from django.db import migrations, models + + +def set_template_language(apps, schema_editor): + """ + Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates). + """ + ExportTemplate = apps.get_model('extras', 'ExportTemplate') + ExportTemplate.objects.update(template_language=10) + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0017_exporttemplate_mime_type_length'), + ] + + operations = [ + migrations.AddField( + model_name='exporttemplate', + name='template_language', + field=models.PositiveSmallIntegerField(default=20), + ), + migrations.RunPython(set_template_language), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index ad31c3821..da8f09a50 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,7 +1,6 @@ from collections import OrderedDict from datetime import date -import graphviz from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -12,6 +11,8 @@ from django.db.models import F, Q from django.http import HttpResponse from django.template import Template, Context from django.urls import reverse +import graphviz +from jinja2 import Environment from dcim.constants import CONNECTION_STATUS_CONNECTED from utilities.utils import deepmerge, foreground_color @@ -355,6 +356,10 @@ class ExportTemplate(models.Model): max_length=200, blank=True ) + template_language = models.PositiveSmallIntegerField( + choices=TEMPLATE_LANGUAGE_CHOICES, + default=TEMPLATE_LANGUAGE_JINJA2 + ) template_code = models.TextField() mime_type = models.CharField( max_length=50, @@ -374,16 +379,36 @@ class ExportTemplate(models.Model): def __str__(self): return '{}: {}'.format(self.content_type, self.name) + def render(self, queryset): + """ + Render the contents of the template. + """ + context = { + 'queryset': queryset + } + + if self.template_language == TEMPLATE_LANGUAGE_DJANGO: + template = Template(self.template_code) + output = template.render(Context(context)) + + elif self.template_language == TEMPLATE_LANGUAGE_JINJA2: + template = Environment().from_string(source=self.template_code) + output = template.render(**context) + + else: + return None + + # 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 """ - template = Template(self.template_code) + output = self.render(queryset) mime_type = 'text/plain' if not self.mime_type else self.mime_type - output = template.render(Context({'queryset': queryset})) - - # Replace CRLF-style line terminators - output = output.replace('\r\n', '\n') # Build the response response = HttpResponse(output, content_type=mime_type) diff --git a/requirements.txt b/requirements.txt index 49e7cf39e..f65328ecd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ django-timezone-field==3.0 djangorestframework==3.9.0 drf-yasg[validation]==1.14.0 graphviz==0.10.1 +Jinja2==2.10 Markdown==2.6.11 netaddr==0.7.19 Pillow==5.3.0