Merge branch 'develop' into develop-2.6

This commit is contained in:
Jeremy Stretch 2019-03-05 13:39:00 -05:00
commit c52d077f92
9 changed files with 62 additions and 56 deletions

View File

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

View File

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

View File

@ -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( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', 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( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='site__region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', 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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='cluster__site__region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', 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(