Merge pull request #3053 from digitalocean/develop

Release v2.5.10
This commit is contained in:
Jeremy Stretch 2019-04-08 14:26:27 -04:00 committed by GitHub
commit f3fffc6161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 137 additions and 42 deletions

View File

@ -1,3 +1,21 @@
v2.5.10 (2019-04-08)
## Enhancements
* [#3052](https://github.com/digitalocean/netbox/issues/3052) - Add Jinja2 support for export templates
## Bug Fixes
* [#2937](https://github.com/digitalocean/netbox/issues/2937) - Redirect to list view after editing an object from list view
* [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces
* [#3039](https://github.com/digitalocean/netbox/issues/3039) - Fix exception when retrieving change object for a component template via API
* [#3041](https://github.com/digitalocean/netbox/issues/3041) - Fix form widget for bulk cable label update
* [#3044](https://github.com/digitalocean/netbox/issues/3044) - Ignore site/rack fields when connecting a new cable via device search
* [#3046](https://github.com/digitalocean/netbox/issues/3046) - Fix exception at reports API endpoint
* [#3047](https://github.com/digitalocean/netbox/issues/3047) - Fix exception when writing mac address for an interface via API
---
v2.5.9 (2019-04-01)
## Enhancements

View File

@ -11,7 +11,7 @@ CIRCUITTYPE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -419,7 +419,9 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
queryset = Interface.objects.select_related(
queryset = Interface.objects.filter(
device__isnull=False
).select_related(
'device', '_connected_interface', '_connected_circuittermination', 'cable'
).prefetch_related(
'ip_addresses', 'tags'

View File

@ -31,7 +31,7 @@ class MACAddressField(models.Field):
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError as e:
raise ValidationError(e)
raise ValidationError("Invalid MAC address format: {}".format(value))
def db_type(self, connection):
return 'macaddr'

View File

@ -2706,12 +2706,12 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
status = forms.ChoiceField(
choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
required=False,
widget=StaticSelect2(),
initial=''
)
label = forms.CharField(
max_length=100,
required=False,
widget=StaticSelect2()
required=False
)
color = forms.CharField(
max_length=6,

View File

@ -44,7 +44,7 @@ REGION_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -56,7 +56,7 @@ RACKGROUP_ACTIONS = """
<i class="fa fa-eye"></i>
</a>
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
<i class="glyphicon glyphicon-pencil"></i>
</a>
{% endif %}
@ -67,7 +67,7 @@ RACKROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -88,7 +88,7 @@ RACKRESERVATION_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -97,7 +97,7 @@ MANUFACTURER_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -106,7 +106,7 @@ DEVICEROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -131,7 +131,7 @@ PLATFORM_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -168,7 +168,7 @@ VIRTUALCHASSIS_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -16,7 +16,8 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG
from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import (
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
ValidatedModelSerializer,
)
from .nested_serializers import *
@ -54,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',
]
#
@ -235,9 +243,14 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
"""
if obj.changed_object is None:
return None
try:
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
if serializer is None:
except SerializerNotFound:
return obj.object_repr
context = {'request': self.context['request']}
context = {
'request': self.context['request']
}
data = serializer(obj.changed_object, context=context).data
return data

View File

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

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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),
]

View File

@ -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)

View File

@ -30,7 +30,7 @@ RIR_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -52,7 +52,7 @@ ROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -152,7 +152,7 @@ VLANGROUP_ACTIONS = """
{% endif %}
{% endwith %}
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -147,18 +147,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
# Miscellaneous
#
def get_view_name(view_cls, suffix=None):
def get_view_name(view, suffix=None):
"""
Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
"""
if hasattr(view_cls, 'queryset'):
if hasattr(view, 'queryset'):
# Determine the model name from the queryset.
name = view_cls.queryset.model._meta.verbose_name
name = view.queryset.model._meta.verbose_name
name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
else:
# Replicate DRF's built-in behavior.
name = view_cls.__name__
name = view.__class__.__name__
name = formatting.remove_trailing_string(name, 'View')
name = formatting.remove_trailing_string(name, 'ViewSet')
name = formatting.camelcase_to_spaces(name)

View File

@ -22,7 +22,7 @@ except ImportError:
)
VERSION = '2.5.9'
VERSION = '2.5.10'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@ -156,11 +156,12 @@ $(document).ready(function() {
filter_for_elements.each(function(index, filter_for_element) {
var param_name = $(filter_for_element).attr(attr_name);
var is_nullable = $(filter_for_element).attr("nullable");
var is_visible = $(filter_for_element).is(":visible");
var value = $(filter_for_element).val();
if (param_name && value) {
if (param_name && is_visible && value) {
parameters[param_name] = value;
} else if (param_name && is_nullable) {
} else if (param_name && is_visible && is_nullable) {
parameters[param_name] = "null";
}
});

View File

@ -8,7 +8,7 @@ SECRETROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -8,7 +8,7 @@ TENANTGROUP_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.tenancy.change_tenantgroup %}
<a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -11,7 +11,7 @@ CLUSTERTYPE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.virtualization.change_clustertype %}
<a href="{% url 'virtualization:clustertype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'virtualization:clustertype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -20,7 +20,7 @@ CLUSTERGROUP_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.virtualization.change_clustergroup %}
<a href="{% url 'virtualization:clustergroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'virtualization:clustergroup_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -622,7 +622,7 @@ class InterfaceTest(APITestCase):
def test_delete_interface(self):
url = reverse('dcim-api:interface-detail', kwargs={'pk': self.interface1.pk})
url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)

View File

@ -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