Bläddra i källkod

Closes #3052: Add Jinja2 support for export templates

Jeremy Stretch 6 år sedan
förälder
incheckning
6f8591f769

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 v2.5.10 (FUTURE)
 v2.5.10 (FUTURE)
 
 
+## Enhancements
+
+* [#3052](https://github.com/digitalocean/netbox/issues/3052) - Add Jinja2 support for export templates
+
 ## Bug Fixes
 ## Bug Fixes
 
 
 * [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces
 * [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces

+ 8 - 1
netbox/extras/api/serializers.py

@@ -55,10 +55,17 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 #
 #
 
 
 class ExportTemplateSerializer(ValidatedModelSerializer):
 class ExportTemplateSerializer(ValidatedModelSerializer):
+    template_language = ChoiceField(
+        choices=TEMPLATE_LANGUAGE_CHOICES,
+        default=TEMPLATE_LANGUAGE_JINJA2
+    )
 
 
     class Meta:
     class Meta:
         model = ExportTemplate
         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',
+        ]
 
 
 
 
 #
 #

+ 1 - 0
netbox/extras/api/views.py

@@ -23,6 +23,7 @@ from . import serializers
 
 
 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
     fields = (
+        (ExportTemplate, ['template_language']),
         (Graph, ['type']),
         (Graph, ['type']),
         (ObjectChange, ['action']),
         (ObjectChange, ['action']),
     )
     )

+ 8 - 0
netbox/extras/constants.py

@@ -56,6 +56,14 @@ EXPORTTEMPLATE_MODELS = [
     'cluster', 'virtualmachine',                                                    # Virtualization
     '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
 # Topology map types
 TOPOLOGYMAP_TYPE_NETWORK = 1
 TOPOLOGYMAP_TYPE_NETWORK = 1
 TOPOLOGYMAP_TYPE_CONSOLE = 2
 TOPOLOGYMAP_TYPE_CONSOLE = 2

+ 1 - 1
netbox/extras/filters.py

@@ -82,7 +82,7 @@ class ExportTemplateFilter(django_filters.FilterSet):
 
 
     class Meta:
     class Meta:
         model = ExportTemplate
         model = ExportTemplate
-        fields = ['content_type', 'name']
+        fields = ['content_type', 'name', 'template_language']
 
 
 
 
 class TagFilter(django_filters.FilterSet):
 class TagFilter(django_filters.FilterSet):

+ 1 - 2
netbox/extras/forms.py

@@ -4,7 +4,6 @@ from django import forms
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-from mptt.forms import TreeNodeMultipleChoiceField
 from taggit.forms import TagField
 from taggit.forms import TagField
 from taggit.models import Tag
 from taggit.models import Tag
 
 
@@ -12,7 +11,7 @@ from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
-    FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
+    FilterChoiceField, LaxURLField, JSONField, SlugField,
 )
 )
 from .constants import (
 from .constants import (
     CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
     CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,

+ 27 - 0
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),
+    ]

+ 31 - 6
netbox/extras/models.py

@@ -1,7 +1,6 @@
 from collections import OrderedDict
 from collections import OrderedDict
 from datetime import date
 from datetime import date
 
 
-import graphviz
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 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.http import HttpResponse
 from django.template import Template, Context
 from django.template import Template, Context
 from django.urls import reverse
 from django.urls import reverse
+import graphviz
+from jinja2 import Environment
 
 
 from dcim.constants import CONNECTION_STATUS_CONNECTED
 from dcim.constants import CONNECTION_STATUS_CONNECTED
 from utilities.utils import deepmerge, foreground_color
 from utilities.utils import deepmerge, foreground_color
@@ -355,6 +356,10 @@ class ExportTemplate(models.Model):
         max_length=200,
         max_length=200,
         blank=True
         blank=True
     )
     )
+    template_language = models.PositiveSmallIntegerField(
+        choices=TEMPLATE_LANGUAGE_CHOICES,
+        default=TEMPLATE_LANGUAGE_JINJA2
+    )
     template_code = models.TextField()
     template_code = models.TextField()
     mime_type = models.CharField(
     mime_type = models.CharField(
         max_length=50,
         max_length=50,
@@ -374,17 +379,37 @@ class ExportTemplate(models.Model):
     def __str__(self):
     def __str__(self):
         return '{}: {}'.format(self.content_type, self.name)
         return '{}: {}'.format(self.content_type, self.name)
 
 
-    def render_to_response(self, queryset):
+    def render(self, queryset):
         """
         """
-        Render the template to an HTTP response, delivered as a named file attachment
+        Render the contents of the template.
         """
         """
-        template = Template(self.template_code)
-        mime_type = 'text/plain' if not self.mime_type else self.mime_type
-        output = template.render(Context({'queryset': queryset}))
+        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
         # Replace CRLF-style line terminators
         output = output.replace('\r\n', '\n')
         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' if not self.mime_type else self.mime_type
+
         # Build the response
         # Build the response
         response = HttpResponse(output, content_type=mime_type)
         response = HttpResponse(output, content_type=mime_type)
         filename = 'netbox_{}{}'.format(
         filename = 'netbox_{}{}'.format(

+ 1 - 0
requirements.txt

@@ -10,6 +10,7 @@ django-timezone-field==3.0
 djangorestframework==3.9.0
 djangorestframework==3.9.0
 drf-yasg[validation]==1.14.0
 drf-yasg[validation]==1.14.0
 graphviz==0.10.1
 graphviz==0.10.1
+Jinja2==2.10
 Markdown==2.6.11
 Markdown==2.6.11
 netaddr==0.7.19
 netaddr==0.7.19
 Pillow==5.3.0
 Pillow==5.3.0