Просмотр исходного кода

Merge branch 'develop' into develop-2.6

Jeremy Stretch 7 лет назад
Родитель
Сommit
c52d077f92

+ 5 - 0
CHANGELOG.md

@@ -31,10 +31,15 @@ v2.5.8 (FUTURE)
 ## Bug Fixes
 ## Bug Fixes
 
 
 * [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs
 * [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs
+* [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions
 * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
 * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
 * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
 * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
 * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
 * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
 * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
 * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
+* [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows
+* [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices
+* [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length
+* [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API
 
 
 ---
 ---
 
 

+ 1 - 1
netbox/dcim/api/serializers.py

@@ -507,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer):
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)
     status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
     status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
-    length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
+    length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Cable
         model = Cable

+ 17 - 34
netbox/dcim/filters.py

@@ -1,6 +1,5 @@
 import django_filters
 import django_filters
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from django.db.models import Q
 from netaddr import EUI
 from netaddr import EUI
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
@@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 from utilities.constants import COLOR_CHOICES
-from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter
+from utilities.filters import (
+    NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
+)
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from .constants import *
 from .constants import *
 from .models import (
 from .models import (
@@ -49,14 +50,15 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
         choices=SITE_STATUS_CHOICES,
         choices=SITE_STATUS_CHOICES,
         null_value=None
         null_value=None
     )
     )
-    region_id = django_filters.NumberFilter(
-        method='filter_region',
-        field_name='pk',
+    region_id = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='region__in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
-    region = django_filters.CharFilter(
-        method='filter_region',
-        field_name='slug',
+    region = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='region__in',
+        to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
     tenant_id = django_filters.ModelMultipleChoiceFilter(
     tenant_id = django_filters.ModelMultipleChoiceFilter(
@@ -95,16 +97,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
             pass
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
-    def filter_region(self, queryset, name, value):
-        try:
-            region = Region.objects.get(**{name: value})
-        except ObjectDoesNotExist:
-            return queryset.none()
-        return queryset.filter(
-            Q(region=region) |
-            Q(region__in=region.get_descendants())
-        )
-
 
 
 class RackGroupFilter(NameSlugSearchFilterSet):
 class RackGroupFilter(NameSlugSearchFilterSet):
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -513,14 +505,15 @@ class DeviceFilter(CustomFieldFilterSet):
     )
     )
     name = NullableCharFieldFilter()
     name = NullableCharFieldFilter()
     asset_tag = NullableCharFieldFilter()
     asset_tag = NullableCharFieldFilter()
-    region_id = django_filters.NumberFilter(
-        method='filter_region',
-        field_name='pk',
+    region_id = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='site__region__in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
-    region = django_filters.CharFilter(
-        method='filter_region',
-        field_name='slug',
+    region = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='site__region__in',
+        to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -619,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet):
             Q(comments__icontains=value)
             Q(comments__icontains=value)
         ).distinct()
         ).distinct()
 
 
-    def filter_region(self, queryset, name, value):
-        try:
-            region = Region.objects.get(**{name: value})
-        except ObjectDoesNotExist:
-            return queryset.none()
-        return queryset.filter(
-            Q(site__region=region) |
-            Q(site__region__in=region.get_descendants())
-        )
-
     def _mac_address(self, queryset, name, value):
     def _mac_address(self, queryset, name, value):
         value = value.strip()
         value = value.strip()
         if not value:
         if not value:

+ 1 - 1
netbox/dcim/models.py

@@ -2416,7 +2416,7 @@ class InventoryItem(ComponentModel):
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (
-            self.device.name or '{' + self.device.pk + '}',
+            self.device.name or '{{{}}}'.format(self.device.pk),
             self.name,
             self.name,
             self.manufacturer.name if self.manufacturer else None,
             self.manufacturer.name if self.manufacturer else None,
             self.part_id,
             self.part_id,

+ 18 - 0
netbox/extras/migrations/0017_exporttemplate_mime_type_length.py

@@ -0,0 +1,18 @@
+# Generated by Django 2.1.7 on 2019-03-05 18:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0016_exporttemplate_add_cable'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='mime_type',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+    ]

+ 1 - 1
netbox/extras/models.py

@@ -359,7 +359,7 @@ class ExportTemplate(models.Model):
     )
     )
     template_code = models.TextField()
     template_code = models.TextField()
     mime_type = models.CharField(
     mime_type = models.CharField(
-        max_length=15,
+        max_length=50,
         blank=True
         blank=True
     )
     )
     file_extension = models.CharField(
     file_extension = models.CharField(

+ 2 - 2
netbox/netbox/settings.py

@@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls'
 TEMPLATES = [
 TEMPLATES = [
     {
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR + '/templates/'],
+        'DIRS': [BASE_DIR + '/templates'],
         'APP_DIRS': True,
         'APP_DIRS': True,
         'OPTIONS': {
         'OPTIONS': {
             'context_processors': [
             'context_processors': [
@@ -223,7 +223,7 @@ USE_I18N = True
 USE_TZ = True
 USE_TZ = True
 
 
 # Static files (CSS, JavaScript, Images)
 # Static files (CSS, JavaScript, Images)
-STATIC_ROOT = BASE_DIR + '/static/'
+STATIC_ROOT = BASE_DIR + '/static'
 STATIC_URL = '/{}static/'.format(BASE_PATH)
 STATIC_URL = '/{}static/'.format(BASE_PATH)
 STATICFILES_DIRS = (
 STATICFILES_DIRS = (
     os.path.join(BASE_DIR, "project-static"),
     os.path.join(BASE_DIR, "project-static"),

+ 9 - 0
netbox/utilities/filters.py

@@ -5,6 +5,15 @@ from django.db.models import Q
 from extras.models import Tag
 from extras.models import Tag
 
 
 
 
+class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
+    """
+    Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]
+    """
+    def filter(self, qs, value):
+        value = [node.get_descendants(include_self=True) for node in value]
+        return super().filter(qs, value)
+
+
 class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
 class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
     """
     """
     Filters for a set of numeric values. Example: id__in=100,200,300
     Filters for a set of numeric values. Example: id__in=100,200,300

+ 8 - 17
netbox/virtualization/filters.py

@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
 from dcim.models import DeviceRole, Interface, Platform, Region, Site
 from dcim.models import DeviceRole, Interface, Platform, Region, Site
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
-from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
+from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from .constants import VM_STATUS_CHOICES
 from .constants import VM_STATUS_CHOICES
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
@@ -119,14 +119,15 @@ class VirtualMachineFilter(CustomFieldFilterSet):
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
         label='Cluster (ID)',
         label='Cluster (ID)',
     )
     )
-    region_id = django_filters.NumberFilter(
-        method='filter_region',
-        field_name='pk',
+    region_id = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='cluster__site__region__in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
-    region = django_filters.CharFilter(
-        method='filter_region',
-        field_name='slug',
+    region = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name='cluster__site__region__in',
+        to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -184,16 +185,6 @@ class VirtualMachineFilter(CustomFieldFilterSet):
             Q(comments__icontains=value)
             Q(comments__icontains=value)
         )
         )
 
 
-    def filter_region(self, queryset, name, value):
-        try:
-            region = Region.objects.get(**{name: value})
-        except ObjectDoesNotExist:
-            return queryset.none()
-        return queryset.filter(
-            Q(cluster__site__region=region) |
-            Q(cluster__site__region__in=region.get_descendants())
-        )
-
 
 
 class InterfaceFilter(django_filters.FilterSet):
 class InterfaceFilter(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(