mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'develop' into develop-2.6
This commit is contained in:
commit
c52d077f92
@ -31,10 +31,15 @@ v2.5.8 (FUTURE)
|
||||
## Bug Fixes
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
|
||||
---
|
||||
|
||||
|
@ -507,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer):
|
||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
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:
|
||||
model = Cable
|
||||
|
@ -1,6 +1,5 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
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 .constants import *
|
||||
from .models import (
|
||||
@ -49,14 +50,15 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
choices=SITE_STATUS_CHOICES,
|
||||
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)',
|
||||
)
|
||||
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)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -95,16 +97,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
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):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -513,14 +505,15 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
)
|
||||
name = 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)',
|
||||
)
|
||||
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)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -619,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).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):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
|
@ -2416,7 +2416,7 @@ class InventoryItem(ComponentModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.name or '{' + self.device.pk + '}',
|
||||
self.device.name or '{{{}}}'.format(self.device.pk),
|
||||
self.name,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.part_id,
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -359,7 +359,7 @@ class ExportTemplate(models.Model):
|
||||
)
|
||||
template_code = models.TextField()
|
||||
mime_type = models.CharField(
|
||||
max_length=15,
|
||||
max_length=50,
|
||||
blank=True
|
||||
)
|
||||
file_extension = models.CharField(
|
||||
|
@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR + '/templates/'],
|
||||
'DIRS': [BASE_DIR + '/templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -223,7 +223,7 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_ROOT = BASE_DIR + '/static'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
|
@ -5,6 +5,15 @@ from django.db.models import Q
|
||||
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):
|
||||
"""
|
||||
Filters for a set of numeric values. Example: id__in=100,200,300
|
||||
|
@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
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 .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
@ -119,14 +119,15 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
||||
queryset=Cluster.objects.all(),
|
||||
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)',
|
||||
)
|
||||
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)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -184,16 +185,6 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
||||
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):
|
||||
q = django_filters.CharFilter(
|
||||
|
Loading…
Reference in New Issue
Block a user