mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-05 11:46:50 -06:00
Merge branch 'develop-2.8' into 3351-plugins
This commit is contained in:
@@ -4,7 +4,9 @@ from django.db.models import Q
|
||||
from dcim.models import Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from .choices import *
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
@@ -16,23 +18,21 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region__in',
|
||||
field_name='circuits__terminations__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region__in',
|
||||
field_name='circuits__terminations__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -65,18 +65,14 @@ class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeFilterSet(NameSlugSearchFilterSet):
|
||||
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -118,12 +114,14 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region__in',
|
||||
field_name='terminations__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region__in',
|
||||
field_name='terminations__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -146,7 +144,7 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilterSet(django_filters.FilterSet):
|
||||
class CircuitTerminationFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -70,11 +70,6 @@ class ProviderTestCase(TestCase):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -194,11 +189,6 @@ class CircuitTestCase(TestCase):
|
||||
params = {'commit_rate': ['1000', '2000']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider(self):
|
||||
provider = Provider.objects.first()
|
||||
params = {'provider_id': [provider.pk]}
|
||||
|
||||
@@ -6,8 +6,8 @@ from tenancy.filters import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.constants import COLOR_CHOICES
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter,
|
||||
TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
|
||||
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
@@ -60,7 +60,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterSet(NameSlugSearchFilterSet):
|
||||
class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
@@ -77,11 +77,7 @@ class RegionFilterSet(NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -92,12 +88,14 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
field_name='region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='region__in',
|
||||
field_name='region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -131,15 +129,17 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class RackGroupFilterSet(NameSlugSearchFilterSet):
|
||||
class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -159,30 +159,28 @@ class RackGroupFilterSet(NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class RackRoleFilterSet(NameSlugSearchFilterSet):
|
||||
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -244,11 +242,7 @@ class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterSet(TenancyFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -305,18 +299,14 @@ class RackReservationFilterSet(TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilterSet(NameSlugSearchFilterSet):
|
||||
class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -410,70 +400,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'name', 'type', 'mgmt_only']
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'name', 'type', 'positions']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet):
|
||||
class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class DeviceRoleFilterSet(NameSlugSearchFilterSet):
|
||||
class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilterSet(NameSlugSearchFilterSet):
|
||||
class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -491,11 +481,13 @@ class PlatformFilterSet(NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver']
|
||||
|
||||
|
||||
class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class DeviceFilterSet(
|
||||
BaseFilterSet,
|
||||
TenancyFilterSet,
|
||||
LocalConfigContextFilterSet,
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -538,12 +530,14 @@ class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomField
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -697,12 +691,14 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -738,7 +734,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilterSet(DeviceComponentFilterSet):
|
||||
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@@ -754,7 +750,7 @@ class ConsolePortFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'description', 'connection_status']
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(DeviceComponentFilterSet):
|
||||
class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@@ -770,7 +766,7 @@ class ConsoleServerPortFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'description', 'connection_status']
|
||||
|
||||
|
||||
class PowerPortFilterSet(DeviceComponentFilterSet):
|
||||
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypeChoices,
|
||||
null_value=None
|
||||
@@ -786,7 +782,7 @@ class PowerPortFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
|
||||
|
||||
|
||||
class PowerOutletFilterSet(DeviceComponentFilterSet):
|
||||
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletTypeChoices,
|
||||
null_value=None
|
||||
@@ -802,7 +798,7 @@ class PowerOutletFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
|
||||
|
||||
|
||||
class InterfaceFilterSet(DeviceComponentFilterSet):
|
||||
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -900,7 +896,7 @@ class InterfaceFilterSet(DeviceComponentFilterSet):
|
||||
}.get(value, queryset.none())
|
||||
|
||||
|
||||
class FrontPortFilterSet(DeviceComponentFilterSet):
|
||||
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -912,7 +908,7 @@ class FrontPortFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(DeviceComponentFilterSet):
|
||||
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@@ -924,26 +920,28 @@ class RearPortFilterSet(DeviceComponentFilterSet):
|
||||
fields = ['id', 'name', 'type', 'positions', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(DeviceComponentFilterSet):
|
||||
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(DeviceComponentFilterSet):
|
||||
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='device__site__region__in',
|
||||
field_name='device__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -1002,19 +1000,21 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(django_filters.FilterSet):
|
||||
class VirtualChassisFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='master__site__region__in',
|
||||
field_name='master__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='master__site__region__in',
|
||||
field_name='master__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -1056,7 +1056,7 @@ class VirtualChassisFilterSet(django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class CableFilterSet(django_filters.FilterSet):
|
||||
class CableFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1119,7 +1119,7 @@ class CableFilterSet(django_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(django_filters.FilterSet):
|
||||
class ConsoleConnectionFilterSet(BaseFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1150,7 +1150,7 @@ class ConsoleConnectionFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(django_filters.FilterSet):
|
||||
class PowerConnectionFilterSet(BaseFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1181,7 +1181,7 @@ class PowerConnectionFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(django_filters.FilterSet):
|
||||
class InterfaceConnectionFilterSet(BaseFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1215,23 +1215,21 @@ class InterfaceConnectionFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelFilterSet(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class PowerPanelFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -1264,23 +1262,21 @@ class PowerPanelFilterSet(django_filters.FilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region__in',
|
||||
field_name='power_panel__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region__in',
|
||||
field_name='power_panel__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
|
||||
@@ -829,6 +829,64 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
return unit_choices
|
||||
|
||||
|
||||
class RackReservationCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
rack_group = forms.CharField(
|
||||
required=False,
|
||||
help_text="Rack's group (if any)"
|
||||
)
|
||||
rack_name = forms.CharField(
|
||||
help_text="Rack name"
|
||||
)
|
||||
units = SimpleArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
required=True,
|
||||
help_text='Comma-separated list of individual unit numbers'
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description')
|
||||
help_texts = {
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
rack_group = self.cleaned_data.get('rack_group')
|
||||
rack_name = self.cleaned_data.get('rack_name')
|
||||
|
||||
# Validate rack
|
||||
if site and rack_group and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
|
||||
elif site and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
|
||||
|
||||
|
||||
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RackReservation.objects.all(),
|
||||
@@ -4621,6 +4679,35 @@ class PowerPanelCSVForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/",
|
||||
filter_for={
|
||||
'rack_group': 'site_id',
|
||||
}
|
||||
)
|
||||
)
|
||||
rack_group = DynamicModelChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/rack-groups/"
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = (
|
||||
'rack_group',
|
||||
)
|
||||
|
||||
|
||||
class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = PowerPanel
|
||||
q = forms.CharField(
|
||||
|
||||
@@ -1,839 +0,0 @@
|
||||
import sys
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
SITE_STATUS_CHOICES = (
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(4, 'retired'),
|
||||
)
|
||||
|
||||
RACK_TYPE_CHOICES = (
|
||||
(100, '2-post-frame'),
|
||||
(200, '4-post-frame'),
|
||||
(300, '4-post-cabinet'),
|
||||
(1000, 'wall-frame'),
|
||||
(1100, 'wall-cabinet'),
|
||||
)
|
||||
|
||||
RACK_STATUS_CHOICES = (
|
||||
(0, 'reserved'),
|
||||
(1, 'available'),
|
||||
(2, 'planned'),
|
||||
(3, 'active'),
|
||||
(4, 'deprecated'),
|
||||
)
|
||||
|
||||
RACK_DIMENSION_CHOICES = (
|
||||
(1000, 'mm'),
|
||||
(2000, 'in'),
|
||||
)
|
||||
|
||||
SUBDEVICE_ROLE_CHOICES = (
|
||||
('true', 'parent'),
|
||||
('false', 'child'),
|
||||
)
|
||||
|
||||
DEVICE_FACE_CHOICES = (
|
||||
(0, 'front'),
|
||||
(1, 'rear'),
|
||||
)
|
||||
|
||||
DEVICE_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(3, 'staged'),
|
||||
(4, 'failed'),
|
||||
(5, 'inventory'),
|
||||
(6, 'decommissioning'),
|
||||
)
|
||||
|
||||
INTERFACE_TYPE_CHOICES = (
|
||||
(0, 'virtual'),
|
||||
(200, 'lag'),
|
||||
(800, '100base-tx'),
|
||||
(1000, '1000base-t'),
|
||||
(1050, '1000base-x-gbic'),
|
||||
(1100, '1000base-x-sfp'),
|
||||
(1120, '2.5gbase-t'),
|
||||
(1130, '5gbase-t'),
|
||||
(1150, '10gbase-t'),
|
||||
(1170, '10gbase-cx4'),
|
||||
(1200, '10gbase-x-sfpp'),
|
||||
(1300, '10gbase-x-xfp'),
|
||||
(1310, '10gbase-x-xenpak'),
|
||||
(1320, '10gbase-x-x2'),
|
||||
(1350, '25gbase-x-sfp28'),
|
||||
(1400, '40gbase-x-qsfpp'),
|
||||
(1420, '50gbase-x-sfp28'),
|
||||
(1500, '100gbase-x-cfp'),
|
||||
(1510, '100gbase-x-cfp2'),
|
||||
(1520, '100gbase-x-cfp4'),
|
||||
(1550, '100gbase-x-cpak'),
|
||||
(1600, '100gbase-x-qsfp28'),
|
||||
(1650, '200gbase-x-cfp2'),
|
||||
(1700, '200gbase-x-qsfp56'),
|
||||
(1750, '400gbase-x-qsfpdd'),
|
||||
(1800, '400gbase-x-osfp'),
|
||||
(2600, 'ieee802.11a'),
|
||||
(2610, 'ieee802.11g'),
|
||||
(2620, 'ieee802.11n'),
|
||||
(2630, 'ieee802.11ac'),
|
||||
(2640, 'ieee802.11ad'),
|
||||
(2810, 'gsm'),
|
||||
(2820, 'cdma'),
|
||||
(2830, 'lte'),
|
||||
(6100, 'sonet-oc3'),
|
||||
(6200, 'sonet-oc12'),
|
||||
(6300, 'sonet-oc48'),
|
||||
(6400, 'sonet-oc192'),
|
||||
(6500, 'sonet-oc768'),
|
||||
(6600, 'sonet-oc1920'),
|
||||
(6700, 'sonet-oc3840'),
|
||||
(3010, '1gfc-sfp'),
|
||||
(3020, '2gfc-sfp'),
|
||||
(3040, '4gfc-sfp'),
|
||||
(3080, '8gfc-sfpp'),
|
||||
(3160, '16gfc-sfpp'),
|
||||
(3320, '32gfc-sfp28'),
|
||||
(3400, '128gfc-sfp28'),
|
||||
(7010, 'inifiband-sdr'),
|
||||
(7020, 'inifiband-ddr'),
|
||||
(7030, 'inifiband-qdr'),
|
||||
(7040, 'inifiband-fdr10'),
|
||||
(7050, 'inifiband-fdr'),
|
||||
(7060, 'inifiband-edr'),
|
||||
(7070, 'inifiband-hdr'),
|
||||
(7080, 'inifiband-ndr'),
|
||||
(7090, 'inifiband-xdr'),
|
||||
(4000, 't1'),
|
||||
(4010, 'e1'),
|
||||
(4040, 't3'),
|
||||
(4050, 'e3'),
|
||||
(5000, 'cisco-stackwise'),
|
||||
(5050, 'cisco-stackwise-plus'),
|
||||
(5100, 'cisco-flexstack'),
|
||||
(5150, 'cisco-flexstack-plus'),
|
||||
(5200, 'juniper-vcp'),
|
||||
(5300, 'extreme-summitstack'),
|
||||
(5310, 'extreme-summitstack-128'),
|
||||
(5320, 'extreme-summitstack-256'),
|
||||
(5330, 'extreme-summitstack-512'),
|
||||
)
|
||||
|
||||
INTERFACE_MODE_CHOICES = (
|
||||
(100, 'access'),
|
||||
(200, 'tagged'),
|
||||
(300, 'tagged-all'),
|
||||
)
|
||||
|
||||
PORT_TYPE_CHOICES = (
|
||||
(1000, '8p8c'),
|
||||
(1100, '110-punch'),
|
||||
(1200, 'bnc'),
|
||||
(2000, 'st'),
|
||||
(2100, 'sc'),
|
||||
(2110, 'sc-apc'),
|
||||
(2200, 'fc'),
|
||||
(2300, 'lc'),
|
||||
(2310, 'lc-apc'),
|
||||
(2400, 'mtrj'),
|
||||
(2500, 'mpo'),
|
||||
(2600, 'lsh'),
|
||||
(2610, 'lsh-apc'),
|
||||
)
|
||||
|
||||
CABLE_TYPE_CHOICES = (
|
||||
(1300, 'cat3'),
|
||||
(1500, 'cat5'),
|
||||
(1510, 'cat5e'),
|
||||
(1600, 'cat6'),
|
||||
(1610, 'cat6a'),
|
||||
(1700, 'cat7'),
|
||||
(1800, 'dac-active'),
|
||||
(1810, 'dac-passive'),
|
||||
(1900, 'coaxial'),
|
||||
(3000, 'mmf'),
|
||||
(3010, 'mmf-om1'),
|
||||
(3020, 'mmf-om2'),
|
||||
(3030, 'mmf-om3'),
|
||||
(3040, 'mmf-om4'),
|
||||
(3500, 'smf'),
|
||||
(3510, 'smf-os1'),
|
||||
(3520, 'smf-os2'),
|
||||
(3800, 'aoc'),
|
||||
(5000, 'power'),
|
||||
)
|
||||
|
||||
CABLE_STATUS_CHOICES = (
|
||||
('true', 'connected'),
|
||||
('false', 'planned'),
|
||||
)
|
||||
|
||||
CABLE_LENGTH_UNIT_CHOICES = (
|
||||
(1200, 'm'),
|
||||
(1100, 'cm'),
|
||||
(2100, 'ft'),
|
||||
(2000, 'in'),
|
||||
)
|
||||
|
||||
POWERFEED_STATUS_CHOICES = (
|
||||
(0, 'offline'),
|
||||
(1, 'active'),
|
||||
(2, 'planned'),
|
||||
(4, 'failed'),
|
||||
)
|
||||
|
||||
POWERFEED_TYPE_CHOICES = (
|
||||
(1, 'primary'),
|
||||
(2, 'redundant'),
|
||||
)
|
||||
|
||||
POWERFEED_SUPPLY_CHOICES = (
|
||||
(1, 'ac'),
|
||||
(2, 'dc'),
|
||||
)
|
||||
|
||||
POWERFEED_PHASE_CHOICES = (
|
||||
(1, 'single-phase'),
|
||||
(3, 'three-phase'),
|
||||
)
|
||||
|
||||
POWEROUTLET_FEED_LEG_CHOICES_CHOICES = (
|
||||
(1, 'A'),
|
||||
(2, 'B'),
|
||||
(3, 'C'),
|
||||
)
|
||||
|
||||
|
||||
def cache_cable_devices(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
|
||||
if 'test' not in sys.argv:
|
||||
print("\nUpdating cable device terminations...")
|
||||
cable_count = Cable.objects.count()
|
||||
|
||||
# Cache A/B termination devices on all existing Cables. Note that the custom save() method on Cable is not
|
||||
# available during a migration, so we replicate its logic here.
|
||||
for i, cable in enumerate(Cable.objects.all(), start=1):
|
||||
|
||||
if not i % 1000 and 'test' not in sys.argv:
|
||||
print("[{}/{}]".format(i, cable_count))
|
||||
|
||||
termination_a_model = apps.get_model(cable.termination_a_type.app_label, cable.termination_a_type.model)
|
||||
termination_a_device = None
|
||||
if hasattr(termination_a_model, 'device'):
|
||||
termination_a = termination_a_model.objects.get(pk=cable.termination_a_id)
|
||||
termination_a_device = termination_a.device
|
||||
|
||||
termination_b_model = apps.get_model(cable.termination_b_type.app_label, cable.termination_b_type.model)
|
||||
termination_b_device = None
|
||||
if hasattr(termination_b_model, 'device'):
|
||||
termination_b = termination_b_model.objects.get(pk=cable.termination_b_id)
|
||||
termination_b_device = termination_b.device
|
||||
|
||||
Cable.objects.filter(pk=cable.pk).update(
|
||||
_termination_a_device=termination_a_device,
|
||||
_termination_b_device=termination_b_device
|
||||
)
|
||||
|
||||
|
||||
def site_status_to_slug(apps, schema_editor):
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
for id, slug in SITE_STATUS_CHOICES:
|
||||
Site.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
def rack_type_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_TYPE_CHOICES:
|
||||
Rack.objects.filter(type=str(id)).update(type=slug)
|
||||
|
||||
|
||||
def rack_status_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_STATUS_CHOICES:
|
||||
Rack.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
def rack_outer_unit_to_slug(apps, schema_editor):
|
||||
Rack = apps.get_model('dcim', 'Rack')
|
||||
for id, slug in RACK_DIMENSION_CHOICES:
|
||||
Rack.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
def devicetype_subdevicerole_to_slug(apps, schema_editor):
|
||||
DeviceType = apps.get_model('dcim', 'DeviceType')
|
||||
for boolean, slug in SUBDEVICE_ROLE_CHOICES:
|
||||
DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug)
|
||||
|
||||
|
||||
def device_face_to_slug(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for id, slug in DEVICE_FACE_CHOICES:
|
||||
Device.objects.filter(face=str(id)).update(face=slug)
|
||||
|
||||
|
||||
def device_status_to_slug(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for id, slug in DEVICE_STATUS_CHOICES:
|
||||
Device.objects.filter(status=str(id)).update(status=slug)
|
||||
|
||||
|
||||
def interfacetemplate_type_to_slug(apps, schema_editor):
|
||||
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
|
||||
for id, slug in INTERFACE_TYPE_CHOICES:
|
||||
InterfaceTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def interface_type_to_slug(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
for id, slug in INTERFACE_TYPE_CHOICES:
|
||||
Interface.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def interface_mode_to_slug(apps, schema_editor):
|
||||
Interface = apps.get_model('dcim', 'Interface')
|
||||
for id, slug in INTERFACE_MODE_CHOICES:
|
||||
Interface.objects.filter(mode=id).update(mode=slug)
|
||||
|
||||
|
||||
def frontporttemplate_type_to_slug(apps, schema_editor):
|
||||
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
FrontPortTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def rearporttemplate_type_to_slug(apps, schema_editor):
|
||||
RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
RearPortTemplate.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def frontport_type_to_slug(apps, schema_editor):
|
||||
FrontPort = apps.get_model('dcim', 'FrontPort')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
FrontPort.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def rearport_type_to_slug(apps, schema_editor):
|
||||
RearPort = apps.get_model('dcim', 'RearPort')
|
||||
for id, slug in PORT_TYPE_CHOICES:
|
||||
RearPort.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def cable_type_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for id, slug in CABLE_TYPE_CHOICES:
|
||||
Cable.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def cable_status_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for bool_str, slug in CABLE_STATUS_CHOICES:
|
||||
Cable.objects.filter(status=bool_str).update(status=slug)
|
||||
|
||||
|
||||
def cable_length_unit_to_slug(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
for id, slug in CABLE_LENGTH_UNIT_CHOICES:
|
||||
Cable.objects.filter(length_unit=id).update(length_unit=slug)
|
||||
|
||||
|
||||
def powerfeed_status_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_STATUS_CHOICES:
|
||||
PowerFeed.objects.filter(status=id).update(status=slug)
|
||||
|
||||
|
||||
def powerfeed_type_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_TYPE_CHOICES:
|
||||
PowerFeed.objects.filter(type=id).update(type=slug)
|
||||
|
||||
|
||||
def powerfeed_supply_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_SUPPLY_CHOICES:
|
||||
PowerFeed.objects.filter(supply=id).update(supply=slug)
|
||||
|
||||
|
||||
def powerfeed_phase_to_slug(apps, schema_editor):
|
||||
PowerFeed = apps.get_model('dcim', 'PowerFeed')
|
||||
for id, slug in POWERFEED_PHASE_CHOICES:
|
||||
PowerFeed.objects.filter(phase=id).update(phase=slug)
|
||||
|
||||
|
||||
def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor):
|
||||
PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
|
||||
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
|
||||
PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug)
|
||||
|
||||
|
||||
def poweroutlet_feed_leg_to_slug(apps, schema_editor):
|
||||
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
|
||||
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
|
||||
PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('dcim', '0071_device_components_add_description'), ('dcim', '0072_powerfeeds'), ('dcim', '0073_interface_form_factor_to_type'), ('dcim', '0074_increase_field_length_platform_name_slug'), ('dcim', '0075_cable_devices'), ('dcim', '0076_console_port_types'), ('dcim', '0077_power_types'), ('dcim', '0078_3569_site_fields'), ('dcim', '0079_3569_rack_fields'), ('dcim', '0080_3569_devicetype_fields'), ('dcim', '0081_3569_device_fields'), ('dcim', '0082_3569_interface_fields'), ('dcim', '0082_3569_port_fields'), ('dcim', '0083_3569_cable_fields'), ('dcim', '0084_3569_powerfeed_fields'), ('dcim', '0085_3569_poweroutlet_fields'), ('dcim', '0086_device_name_nonunique'), ('dcim', '0087_role_descriptions'), ('dcim', '0088_powerfeed_available_power')]
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0070_custom_tag_models'),
|
||||
('extras', '0021_add_color_comments_changelog_to_tag'),
|
||||
('tenancy', '0006_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPanel',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('rack_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
'unique_together': {('site', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerFeed',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('status', models.PositiveSmallIntegerField(default=1)),
|
||||
('type', models.PositiveSmallIntegerField(default=1)),
|
||||
('supply', models.PositiveSmallIntegerField(default=1)),
|
||||
('phase', models.PositiveSmallIntegerField(default=1)),
|
||||
('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
|
||||
('available_power', models.PositiveSmallIntegerField(default=0, editable=False)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')),
|
||||
('power_panel', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel')),
|
||||
('rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack')),
|
||||
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags')),
|
||||
('connected_endpoint', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort')),
|
||||
('connection_status', models.NullBooleanField()),
|
||||
],
|
||||
options={
|
||||
'ordering': ['power_panel', 'name'],
|
||||
'unique_together': {('power_panel', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='powerport',
|
||||
old_name='connected_endpoint',
|
||||
new_name='_connected_poweroutlet',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='_connected_powerfeed',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='allocated_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='maximum_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='allocated_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='maximum_draw',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='power_port',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='power_port',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='interface',
|
||||
old_name='form_factor',
|
||||
new_name='type',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='interfacetemplate',
|
||||
old_name='form_factor',
|
||||
new_name='type',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='_termination_a_device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='_termination_b_device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cache_cable_devices,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=site_status_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_status_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rack_outer_unit_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='outer_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=devicetype_subdevicerole_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicetype',
|
||||
name='subdevice_role',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=device_face_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='face',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=device_status_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interfacetemplate_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interface_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=interface_mode_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='mode',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=frontporttemplate_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rearporttemplate_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=frontport_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rearport_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='status',
|
||||
field=models.CharField(default='connected', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_status_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length_unit',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=cable_length_unit_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='status',
|
||||
field=models.CharField(default='active', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_status_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='type',
|
||||
field=models.CharField(default='primary', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_type_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='supply',
|
||||
field=models.CharField(default='ac', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_supply_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='phase',
|
||||
field=models.CharField(default='single-phase', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=powerfeed_phase_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=poweroutlettemplate_feed_leg_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=poweroutlet_feed_leg_to_slug,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='feed_leg',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together={('rack', 'position', 'face'), ('site', 'tenant', 'name'), ('virtual_chassis', 'vc_position')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerfeed',
|
||||
name='available_power',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
||||
@@ -761,6 +761,8 @@ class RackReservation(ChangeLoggedModel):
|
||||
max_length=100
|
||||
)
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['created']
|
||||
|
||||
@@ -793,6 +795,17 @@ class RackReservation(ChangeLoggedModel):
|
||||
)
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.rack.site.name,
|
||||
self.rack.group if self.rack.group else None,
|
||||
self.rack.name,
|
||||
','.join([str(u) for u in self.units]),
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.user.username,
|
||||
self.description
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_list(self):
|
||||
"""
|
||||
|
||||
@@ -138,11 +138,6 @@ class SiteTestCase(TestCase):
|
||||
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -365,11 +360,6 @@ class RackTestCase(TestCase):
|
||||
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -479,11 +469,6 @@ class RackReservationTestCase(TestCase):
|
||||
)
|
||||
RackReservation.objects.bulk_create(reservations)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -631,11 +616,6 @@ class DeviceTypeTestCase(TestCase):
|
||||
params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_manufacturer(self):
|
||||
manufacturers = Manufacturer.objects.all()[:2]
|
||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||
@@ -1283,11 +1263,6 @@ class DeviceTestCase(TestCase):
|
||||
params = {'vc_priority': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_manufacturer(self):
|
||||
manufacturers = Manufacturer.objects.all()[:2]
|
||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||
|
||||
@@ -19,14 +19,9 @@ class NaturalOrderingTestCase(TestCase):
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
|
||||
def _compare_names(self, queryset, names):
|
||||
|
||||
for i, obj in enumerate(queryset):
|
||||
self.assertEqual(obj.name, names[i])
|
||||
|
||||
def test_interface_ordering_numeric(self):
|
||||
|
||||
INTERFACES = (
|
||||
INTERFACES = [
|
||||
'0',
|
||||
'0.1',
|
||||
'0.2',
|
||||
@@ -53,17 +48,20 @@ class NaturalOrderingTestCase(TestCase):
|
||||
'1:2.1',
|
||||
'1:2.2',
|
||||
'1:2.10',
|
||||
)
|
||||
]
|
||||
|
||||
for name in INTERFACES:
|
||||
iface = Interface(device=self.device, name=name)
|
||||
iface.save()
|
||||
|
||||
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
|
||||
self.assertListEqual(
|
||||
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
|
||||
INTERFACES
|
||||
)
|
||||
|
||||
def test_interface_ordering_linux(self):
|
||||
|
||||
INTERFACES = (
|
||||
INTERFACES = [
|
||||
'eth0',
|
||||
'eth0.1',
|
||||
'eth0.2',
|
||||
@@ -74,17 +72,20 @@ class NaturalOrderingTestCase(TestCase):
|
||||
'eth1.2',
|
||||
'eth1.100',
|
||||
'lo0',
|
||||
)
|
||||
]
|
||||
|
||||
for name in INTERFACES:
|
||||
iface = Interface(device=self.device, name=name)
|
||||
iface.save()
|
||||
|
||||
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
|
||||
self.assertListEqual(
|
||||
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
|
||||
INTERFACES
|
||||
)
|
||||
|
||||
def test_interface_ordering_junos(self):
|
||||
|
||||
INTERFACES = (
|
||||
INTERFACES = [
|
||||
'xe-0/0/0',
|
||||
'xe-0/0/1',
|
||||
'xe-0/0/2',
|
||||
@@ -124,17 +125,20 @@ class NaturalOrderingTestCase(TestCase):
|
||||
'irb.10',
|
||||
'irb.100',
|
||||
'lo0',
|
||||
)
|
||||
]
|
||||
|
||||
for name in INTERFACES:
|
||||
iface = Interface(device=self.device, name=name)
|
||||
iface.save()
|
||||
|
||||
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
|
||||
self.assertListEqual(
|
||||
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
|
||||
INTERFACES
|
||||
)
|
||||
|
||||
def test_interface_ordering_ios(self):
|
||||
|
||||
INTERFACES = (
|
||||
INTERFACES = [
|
||||
'GigabitEthernet0/1',
|
||||
'GigabitEthernet0/2',
|
||||
'GigabitEthernet0/10',
|
||||
@@ -148,10 +152,13 @@ class NaturalOrderingTestCase(TestCase):
|
||||
'FastEthernet1',
|
||||
'FastEthernet2',
|
||||
'FastEthernet10',
|
||||
)
|
||||
]
|
||||
|
||||
for name in INTERFACES:
|
||||
iface = Interface(device=self.device, name=name)
|
||||
iface.save()
|
||||
|
||||
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
|
||||
self.assertListEqual(
|
||||
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
|
||||
INTERFACES
|
||||
)
|
||||
|
||||
@@ -176,9 +176,6 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
test_get_object = None
|
||||
test_create_object = None
|
||||
|
||||
# TODO: Fix URL name for view
|
||||
test_import_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -204,6 +201,13 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'description': 'Rack reservation',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
'site,rack_name,units,description',
|
||||
'Site 1,Rack 1,"10,11,12",Reservation 1',
|
||||
'Site 1,Rack 1,"13,14,15",Reservation 2',
|
||||
'Site 1,Rack 1,"16,17,18",Reservation 3',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'user': user3.pk,
|
||||
'tenant': None,
|
||||
@@ -1553,9 +1557,6 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = PowerPanel
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -1590,6 +1591,11 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
"Site 1,Rack Group 1,Power Panel 6",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'site': sites[1].pk,
|
||||
'rack_group': rackgroups[1].pk,
|
||||
}
|
||||
|
||||
|
||||
class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = PowerFeed
|
||||
|
||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
||||
|
||||
# Rack reservations
|
||||
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
|
||||
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
@@ -331,6 +332,7 @@ urlpatterns = [
|
||||
path('power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
|
||||
path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
|
||||
path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
|
||||
path('power-panels/edit/', views.PowerPanelBulkEditView.as_view(), name='powerpanel_bulk_edit'),
|
||||
path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
|
||||
path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
|
||||
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||
|
||||
@@ -470,7 +470,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
action_buttons = ()
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -500,6 +500,23 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_rackreservation'
|
||||
model_form = forms.RackReservationCSVForm
|
||||
table = tables.RackReservationTable
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
def _save_obj(self, obj_form, request):
|
||||
"""
|
||||
Assign the currently authenticated user to the RackReservation.
|
||||
"""
|
||||
instance = obj_form.save(commit=False)
|
||||
instance.user = request.user
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
@@ -1245,7 +1262,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
template_name = 'dcim/device_import_child.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
def _save_obj(self, obj_form):
|
||||
def _save_obj(self, obj_form, request):
|
||||
|
||||
obj = obj_form.save()
|
||||
|
||||
@@ -1316,6 +1333,7 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
form = forms.ConsolePortBulkEditForm
|
||||
|
||||
@@ -1323,6 +1341,7 @@ class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
default_return_url = 'dcim:consoleport_list'
|
||||
|
||||
@@ -1369,6 +1388,7 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
form = forms.ConsoleServerPortBulkEditForm
|
||||
|
||||
@@ -1388,6 +1408,7 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
default_return_url = 'dcim:consoleserverport_list'
|
||||
|
||||
@@ -1434,6 +1455,7 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
form = forms.PowerPortBulkEditForm
|
||||
|
||||
@@ -1441,6 +1463,7 @@ class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
default_return_url = 'dcim:powerport_list'
|
||||
|
||||
@@ -1487,6 +1510,7 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
form = forms.PowerOutletBulkEditForm
|
||||
|
||||
@@ -1506,6 +1530,7 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
default_return_url = 'dcim:poweroutlet_list'
|
||||
|
||||
@@ -1589,6 +1614,7 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
@@ -1608,6 +1634,7 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
default_return_url = 'dcim:interface_list'
|
||||
|
||||
@@ -1654,6 +1681,7 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
@@ -1673,6 +1701,7 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
default_return_url = 'dcim:frontport_list'
|
||||
|
||||
@@ -1719,6 +1748,7 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
@@ -1738,6 +1768,7 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
default_return_url = 'dcim:rearport_list'
|
||||
|
||||
@@ -1861,6 +1892,7 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
default_return_url = 'dcim:devicebay_list'
|
||||
|
||||
@@ -2569,6 +2601,15 @@ class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
class PowerPanelBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_powerpanel'
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
default_return_url = 'dcim:powerpanel_list'
|
||||
|
||||
|
||||
class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerpanel'
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CreateOnlyDefault
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||
@@ -14,6 +16,43 @@ from utilities.api import ValidatedModelSerializer
|
||||
# Custom fields
|
||||
#
|
||||
|
||||
class CustomFieldDefaultValues:
|
||||
"""
|
||||
Return a dictionary of all CustomFields assigned to the parent model and their default values.
|
||||
"""
|
||||
def __call__(self):
|
||||
|
||||
# Retrieve the CustomFields for the parent model
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
# Populate the default value for each CustomField
|
||||
value = {}
|
||||
for field in fields:
|
||||
if field.default:
|
||||
if field.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
field_value = int(field.default)
|
||||
elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
# TODO: Fix default value assignment for boolean custom fields
|
||||
field_value = False if field.default.lower() == 'false' else bool(field.default)
|
||||
elif field.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
try:
|
||||
field_value = field.choices.get(value=field.default).pk
|
||||
except ObjectDoesNotExist:
|
||||
# Invalid default value
|
||||
field_value = None
|
||||
else:
|
||||
field_value = field.default
|
||||
value[field.name] = field_value
|
||||
else:
|
||||
value[field.name] = None
|
||||
|
||||
return value
|
||||
|
||||
def set_context(self, serializer_field):
|
||||
self.model = serializer_field.parent.Meta.model
|
||||
|
||||
|
||||
class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
def to_representation(self, obj):
|
||||
@@ -94,53 +133,35 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
custom_fields = CustomFieldsSerializer(required=False)
|
||||
custom_fields = CustomFieldsSerializer(
|
||||
required=False,
|
||||
default=CreateOnlyDefault(CustomFieldDefaultValues())
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
def _populate_custom_fields(instance, fields):
|
||||
instance.custom_fields = {}
|
||||
for field in fields:
|
||||
value = instance.cf.get(field.name)
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||
else:
|
||||
instance.custom_fields[field.name] = value
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
if self.instance is not None:
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
for obj in self.instance:
|
||||
_populate_custom_fields(obj, fields)
|
||||
self._populate_custom_fields(obj, fields)
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
self._populate_custom_fields(self.instance, fields)
|
||||
|
||||
else:
|
||||
|
||||
if not hasattr(self, 'initial_data'):
|
||||
self.initial_data = {}
|
||||
|
||||
# Populate default values
|
||||
if fields and 'custom_fields' not in self.initial_data:
|
||||
self.initial_data['custom_fields'] = {}
|
||||
|
||||
# Populate initial data using custom field default values
|
||||
for field in fields:
|
||||
if field.name not in self.initial_data['custom_fields'] and field.default:
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
field_value = field.choices.get(value=field.default).pk
|
||||
elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
field_value = bool(field.default)
|
||||
else:
|
||||
field_value = field.default
|
||||
self.initial_data['custom_fields'][field.name] = field_value
|
||||
def _populate_custom_fields(self, instance, custom_fields):
|
||||
instance.custom_fields = {}
|
||||
for field in custom_fields:
|
||||
value = instance.cf.get(field.name)
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||
else:
|
||||
instance.custom_fields[field.name] = value
|
||||
|
||||
def _save_custom_fields(self, instance, custom_fields):
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
||||
@@ -89,21 +90,21 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||
|
||||
|
||||
class GraphFilterSet(django_filters.FilterSet):
|
||||
class GraphFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
fields = ['type', 'name', 'template_language']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(django_filters.FilterSet):
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['content_type', 'name', 'template_language']
|
||||
|
||||
|
||||
class TagFilterSet(django_filters.FilterSet):
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -122,7 +123,7 @@ class TagFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextFilterSet(django_filters.FilterSet):
|
||||
class ConfigContextFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -244,7 +245,7 @@ class LocalConfigContextFilterSet(django_filters.FilterSet):
|
||||
return queryset.exclude(local_context_data__isnull=value)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(django_filters.FilterSet):
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -451,7 +451,8 @@ def get_scripts(use_names=False):
|
||||
module_scripts = OrderedDict()
|
||||
for name, cls in inspect.getmembers(module, is_script):
|
||||
module_scripts[name] = cls
|
||||
scripts[module_name] = module_scripts
|
||||
if module_scripts:
|
||||
scripts[module_name] = module_scripts
|
||||
|
||||
return scripts
|
||||
|
||||
|
||||
@@ -101,240 +101,329 @@ class CustomFieldTest(TestCase):
|
||||
|
||||
class CustomFieldAPITest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
# Text custom field
|
||||
self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word')
|
||||
self.cf_text.save()
|
||||
self.cf_text.obj_type.set([content_type])
|
||||
self.cf_text.save()
|
||||
cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
|
||||
cls.cf_text.save()
|
||||
cls.cf_text.obj_type.set([content_type])
|
||||
|
||||
# Integer custom field
|
||||
self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number')
|
||||
self.cf_integer.save()
|
||||
self.cf_integer.obj_type.set([content_type])
|
||||
self.cf_integer.save()
|
||||
cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123)
|
||||
cls.cf_integer.save()
|
||||
cls.cf_integer.obj_type.set([content_type])
|
||||
|
||||
# Boolean custom field
|
||||
self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic')
|
||||
self.cf_boolean.save()
|
||||
self.cf_boolean.obj_type.set([content_type])
|
||||
self.cf_boolean.save()
|
||||
cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False)
|
||||
cls.cf_boolean.save()
|
||||
cls.cf_boolean.obj_type.set([content_type])
|
||||
|
||||
# Date custom field
|
||||
self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date')
|
||||
self.cf_date.save()
|
||||
self.cf_date.obj_type.set([content_type])
|
||||
self.cf_date.save()
|
||||
cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01')
|
||||
cls.cf_date.save()
|
||||
cls.cf_date.obj_type.set([content_type])
|
||||
|
||||
# URL custom field
|
||||
self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url')
|
||||
self.cf_url.save()
|
||||
self.cf_url.obj_type.set([content_type])
|
||||
self.cf_url.save()
|
||||
cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1')
|
||||
cls.cf_url.save()
|
||||
cls.cf_url.obj_type.set([content_type])
|
||||
|
||||
# Select custom field
|
||||
self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice')
|
||||
self.cf_select.save()
|
||||
self.cf_select.obj_type.set([content_type])
|
||||
self.cf_select.save()
|
||||
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
|
||||
self.cf_select_choice1.save()
|
||||
self.cf_select_choice2 = CustomFieldChoice(field=self.cf_select, value='Bar')
|
||||
self.cf_select_choice2.save()
|
||||
self.cf_select_choice3 = CustomFieldChoice(field=self.cf_select, value='Baz')
|
||||
self.cf_select_choice3.save()
|
||||
cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field')
|
||||
cls.cf_select.save()
|
||||
cls.cf_select.obj_type.set([content_type])
|
||||
cls.cf_select_choice1 = CustomFieldChoice(field=cls.cf_select, value='Foo')
|
||||
cls.cf_select_choice1.save()
|
||||
cls.cf_select_choice2 = CustomFieldChoice(field=cls.cf_select, value='Bar')
|
||||
cls.cf_select_choice2.save()
|
||||
cls.cf_select_choice3 = CustomFieldChoice(field=cls.cf_select, value='Baz')
|
||||
cls.cf_select_choice3.save()
|
||||
|
||||
self.site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
cls.cf_select.default = cls.cf_select_choice1.value
|
||||
cls.cf_select.save()
|
||||
|
||||
def test_get_obj_without_custom_fields(self):
|
||||
# Create some sites
|
||||
cls.sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'], {
|
||||
'magic_word': None,
|
||||
'magic_number': None,
|
||||
'is_magic': None,
|
||||
'magic_date': None,
|
||||
'magic_url': None,
|
||||
'magic_choice': None,
|
||||
})
|
||||
|
||||
def test_get_obj_with_custom_fields(self):
|
||||
|
||||
CUSTOM_FIELD_VALUES = [
|
||||
(self.cf_text, 'Test string'),
|
||||
(self.cf_integer, 1234),
|
||||
(self.cf_boolean, True),
|
||||
(self.cf_date, date(2016, 6, 23)),
|
||||
(self.cf_url, 'http://example.com/'),
|
||||
(self.cf_select, self.cf_select_choice1.pk),
|
||||
]
|
||||
for field, value in CUSTOM_FIELD_VALUES:
|
||||
cfv = CustomFieldValue(field=field, obj=self.site)
|
||||
# Assign custom field values for site 2
|
||||
site2_cfvs = {
|
||||
cls.cf_text: 'bar',
|
||||
cls.cf_integer: 456,
|
||||
cls.cf_boolean: True,
|
||||
cls.cf_date: '2020-01-02',
|
||||
cls.cf_url: 'http://example.com/2',
|
||||
cls.cf_select: cls.cf_select_choice2.pk,
|
||||
}
|
||||
for field, value in site2_cfvs.items():
|
||||
cfv = CustomFieldValue(field=field, obj=cls.sites[1])
|
||||
cfv.value = value
|
||||
cfv.save()
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
def test_get_single_object_without_custom_field_values(self):
|
||||
"""
|
||||
Validate that custom fields are present on an object even if it has no values defined.
|
||||
"""
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[0].pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), {
|
||||
'value': self.cf_select_choice1.pk, 'label': 'Foo'
|
||||
self.assertEqual(response.data['name'], self.sites[0].name)
|
||||
self.assertEqual(response.data['custom_fields'], {
|
||||
'text_field': None,
|
||||
'number_field': None,
|
||||
'boolean_field': None,
|
||||
'date_field': None,
|
||||
'url_field': None,
|
||||
'choice_field': None,
|
||||
})
|
||||
|
||||
def test_set_custom_field_text(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_word': 'Foo bar baz',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), data['custom_fields']['magic_word'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_text)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_word'])
|
||||
|
||||
def test_set_custom_field_integer(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_number': 42,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), data['custom_fields']['magic_number'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_integer)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_number'])
|
||||
|
||||
def test_set_custom_field_boolean(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'is_magic': 0,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), data['custom_fields']['is_magic'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_boolean)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['is_magic'])
|
||||
|
||||
def test_set_custom_field_date(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_date': '2017-04-25',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_date)
|
||||
self.assertEqual(cfv.value.isoformat(), data['custom_fields']['magic_date'])
|
||||
|
||||
def test_set_custom_field_url(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_url': 'http://example.com/2/',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), data['custom_fields']['magic_url'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_url)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_url'])
|
||||
|
||||
def test_set_custom_field_select(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_choice': self.cf_select_choice2.pk,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_select)
|
||||
self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice'])
|
||||
|
||||
def test_set_custom_field_defaults(self):
|
||||
def test_get_single_object_with_custom_field_values(self):
|
||||
"""
|
||||
Create a new object with no custom field data. Custom field values should be created using the custom fields'
|
||||
default values.
|
||||
Validate that custom fields are present and correctly set for an object with values defined.
|
||||
"""
|
||||
CUSTOM_FIELD_DEFAULTS = {
|
||||
'magic_word': 'foobar',
|
||||
'magic_number': '123',
|
||||
'is_magic': 'true',
|
||||
'magic_date': '2019-12-13',
|
||||
'magic_url': 'http://example.com/',
|
||||
'magic_choice': self.cf_select_choice1.value,
|
||||
site2_cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all()
|
||||
}
|
||||
|
||||
# Update CustomFields to set default values
|
||||
for field_name, default_value in CUSTOM_FIELD_DEFAULTS.items():
|
||||
CustomField.objects.filter(name=field_name).update(default=default_value)
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.sites[1].name)
|
||||
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
||||
self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
|
||||
self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
|
||||
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
||||
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
||||
self.assertEqual(response.data['custom_fields']['choice_field']['label'], self.cf_select_choice2.value)
|
||||
|
||||
def test_create_single_object_with_defaults(self):
|
||||
"""
|
||||
Create a new site with no specified custom field values and check that it received the default values.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Test Site X',
|
||||
'slug': 'test-site-x',
|
||||
'name': 'Site 3',
|
||||
'slug': 'site-3',
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['custom_fields']['magic_word'], CUSTOM_FIELD_DEFAULTS['magic_word'])
|
||||
self.assertEqual(response.data['custom_fields']['magic_number'], str(CUSTOM_FIELD_DEFAULTS['magic_number']))
|
||||
self.assertEqual(response.data['custom_fields']['is_magic'], bool(CUSTOM_FIELD_DEFAULTS['is_magic']))
|
||||
self.assertEqual(response.data['custom_fields']['magic_date'], CUSTOM_FIELD_DEFAULTS['magic_date'])
|
||||
self.assertEqual(response.data['custom_fields']['magic_url'], CUSTOM_FIELD_DEFAULTS['magic_url'])
|
||||
self.assertEqual(response.data['custom_fields']['magic_choice'], self.cf_select_choice1.pk)
|
||||
|
||||
# Validate response data
|
||||
response_cf = response.data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], self.cf_text.default)
|
||||
self.assertEqual(response_cf['number_field'], self.cf_integer.default)
|
||||
self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
|
||||
self.assertEqual(response_cf['date_field'], self.cf_date.default)
|
||||
self.assertEqual(response_cf['url_field'], self.cf_url.default)
|
||||
self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk)
|
||||
|
||||
# Validate database data
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in site.custom_field_values.all()
|
||||
}
|
||||
self.assertEqual(cfvs['text_field'], self.cf_text.default)
|
||||
self.assertEqual(cfvs['number_field'], self.cf_integer.default)
|
||||
self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default)
|
||||
self.assertEqual(str(cfvs['date_field']), self.cf_date.default)
|
||||
self.assertEqual(cfvs['url_field'], self.cf_url.default)
|
||||
self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk)
|
||||
|
||||
def test_create_single_object_with_values(self):
|
||||
"""
|
||||
Create a single new site with a value for each type of custom field.
|
||||
"""
|
||||
data = {
|
||||
'name': 'Site 3',
|
||||
'slug': 'site-3',
|
||||
'custom_fields': {
|
||||
'text_field': 'bar',
|
||||
'number_field': 456,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
'choice_field': self.cf_select_choice2.pk,
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
# Validate response data
|
||||
response_cf = response.data['custom_fields']
|
||||
data_cf = data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(response_cf['number_field'], data_cf['number_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
||||
self.assertEqual(response_cf['choice_field'], data_cf['choice_field'])
|
||||
|
||||
# Validate database data
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in site.custom_field_values.all()
|
||||
}
|
||||
self.assertEqual(cfvs['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(cfvs['number_field'], data_cf['number_field'])
|
||||
self.assertEqual(cfvs['boolean_field'], data_cf['boolean_field'])
|
||||
self.assertEqual(str(cfvs['date_field']), data_cf['date_field'])
|
||||
self.assertEqual(cfvs['url_field'], data_cf['url_field'])
|
||||
self.assertEqual(cfvs['choice_field'].pk, data_cf['choice_field'])
|
||||
|
||||
def test_create_multiple_objects_with_defaults(self):
|
||||
"""
|
||||
Create three news sites with no specified custom field values and check that each received
|
||||
the default custom field values.
|
||||
"""
|
||||
data = (
|
||||
{
|
||||
'name': 'Site 3',
|
||||
'slug': 'site-3',
|
||||
},
|
||||
{
|
||||
'name': 'Site 4',
|
||||
'slug': 'site-4',
|
||||
},
|
||||
{
|
||||
'name': 'Site 5',
|
||||
'slug': 'site-5',
|
||||
},
|
||||
)
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(len(response.data), len(data))
|
||||
|
||||
for i, obj in enumerate(data):
|
||||
|
||||
# Validate response data
|
||||
response_cf = response.data[i]['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], self.cf_text.default)
|
||||
self.assertEqual(response_cf['number_field'], self.cf_integer.default)
|
||||
self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
|
||||
self.assertEqual(response_cf['date_field'], self.cf_date.default)
|
||||
self.assertEqual(response_cf['url_field'], self.cf_url.default)
|
||||
self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk)
|
||||
|
||||
# Validate database data
|
||||
site = Site.objects.get(pk=response.data[i]['id'])
|
||||
cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in site.custom_field_values.all()
|
||||
}
|
||||
self.assertEqual(cfvs['text_field'], self.cf_text.default)
|
||||
self.assertEqual(cfvs['number_field'], self.cf_integer.default)
|
||||
self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default)
|
||||
self.assertEqual(str(cfvs['date_field']), self.cf_date.default)
|
||||
self.assertEqual(cfvs['url_field'], self.cf_url.default)
|
||||
self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk)
|
||||
|
||||
def test_create_multiple_objects_with_values(self):
|
||||
"""
|
||||
Create a three new sites, each with custom fields defined.
|
||||
"""
|
||||
custom_field_data = {
|
||||
'text_field': 'bar',
|
||||
'number_field': 456,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
'choice_field': self.cf_select_choice2.pk,
|
||||
}
|
||||
data = (
|
||||
{
|
||||
'name': 'Site 3',
|
||||
'slug': 'site-3',
|
||||
'custom_fields': custom_field_data,
|
||||
},
|
||||
{
|
||||
'name': 'Site 4',
|
||||
'slug': 'site-4',
|
||||
'custom_fields': custom_field_data,
|
||||
},
|
||||
{
|
||||
'name': 'Site 5',
|
||||
'slug': 'site-5',
|
||||
'custom_fields': custom_field_data,
|
||||
},
|
||||
)
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(len(response.data), len(data))
|
||||
|
||||
for i, obj in enumerate(data):
|
||||
|
||||
# Validate response data
|
||||
response_cf = response.data[i]['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
|
||||
self.assertEqual(response_cf['number_field'], custom_field_data['number_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
||||
self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field'])
|
||||
|
||||
# Validate database data
|
||||
site = Site.objects.get(pk=response.data[i]['id'])
|
||||
cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in site.custom_field_values.all()
|
||||
}
|
||||
self.assertEqual(cfvs['text_field'], custom_field_data['text_field'])
|
||||
self.assertEqual(cfvs['number_field'], custom_field_data['number_field'])
|
||||
self.assertEqual(cfvs['boolean_field'], custom_field_data['boolean_field'])
|
||||
self.assertEqual(str(cfvs['date_field']), custom_field_data['date_field'])
|
||||
self.assertEqual(cfvs['url_field'], custom_field_data['url_field'])
|
||||
self.assertEqual(cfvs['choice_field'].pk, custom_field_data['choice_field'])
|
||||
|
||||
def test_update_single_object_with_values(self):
|
||||
"""
|
||||
Update an object with existing custom field values. Ensure that only the updated custom field values are
|
||||
modified.
|
||||
"""
|
||||
site2_original_cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all()
|
||||
}
|
||||
data = {
|
||||
'custom_fields': {
|
||||
'text_field': 'ABCD',
|
||||
'number_field': 1234,
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
# Validate response data
|
||||
response_cf = response.data['custom_fields']
|
||||
data_cf = data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(response_cf['number_field'], data_cf['number_field'])
|
||||
# TODO: Non-updated fields are missing from the response data
|
||||
# self.assertEqual(response_cf['boolean_field'], site2_original_cfvs['boolean_field'])
|
||||
# self.assertEqual(response_cf['date_field'], site2_original_cfvs['date_field'])
|
||||
# self.assertEqual(response_cf['url_field'], site2_original_cfvs['url_field'])
|
||||
# self.assertEqual(response_cf['choice_field']['label'], site2_original_cfvs['choice_field'].value)
|
||||
|
||||
# Validate database data
|
||||
site2_updated_cfvs = {
|
||||
cfv.field.name: cfv.value for cfv in self.sites[1].custom_field_values.all()
|
||||
}
|
||||
self.assertEqual(site2_updated_cfvs['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(site2_updated_cfvs['number_field'], data_cf['number_field'])
|
||||
self.assertEqual(site2_updated_cfvs['boolean_field'], site2_original_cfvs['boolean_field'])
|
||||
self.assertEqual(site2_updated_cfvs['date_field'], site2_original_cfvs['date_field'])
|
||||
self.assertEqual(site2_updated_cfvs['url_field'], site2_original_cfvs['url_field'])
|
||||
self.assertEqual(site2_updated_cfvs['choice_field'], site2_original_cfvs['choice_field'])
|
||||
|
||||
|
||||
class CustomFieldChoiceAPITest(APITestCase):
|
||||
|
||||
@@ -28,8 +28,8 @@ class GraphTestCase(TestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': 'Graph 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'name': ['Graph 1', 'Graph 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
content_type = ContentType.objects.filter(GRAPH_MODELS).first()
|
||||
@@ -59,8 +59,8 @@ class ExportTemplateTestCase(TestCase):
|
||||
ExportTemplate.objects.bulk_create(export_templates)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': 'Export Template 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
||||
@@ -154,8 +154,8 @@ class ConfigContextTestCase(TestCase):
|
||||
c.tenants.set([tenants[i]])
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': 'Config Context 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'name': ['Config Context 1', 'Config Context 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_is_active(self):
|
||||
params = {'is_active': True}
|
||||
|
||||
@@ -8,7 +8,8 @@ from dcim.models import Device, Interface, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .choices import *
|
||||
@@ -28,11 +29,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -53,22 +50,14 @@ class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
|
||||
fields = ['name', 'rd', 'enforce_unique']
|
||||
|
||||
|
||||
class RIRFilterSet(NameSlugSearchFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['name', 'slug', 'is_private']
|
||||
|
||||
|
||||
class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -114,11 +103,11 @@ class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except ValidationError:
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class RoleFilterSet(NameSlugSearchFilterSet):
|
||||
class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -129,11 +118,7 @@ class RoleFilterSet(NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -174,12 +159,14 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -238,7 +225,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except ValidationError:
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
def search_within(self, queryset, name, value):
|
||||
@@ -281,11 +268,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
||||
return queryset.filter(prefix__net_mask_length=value)
|
||||
|
||||
|
||||
class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -409,15 +392,17 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
||||
return queryset.exclude(interface__isnull=value)
|
||||
|
||||
|
||||
class VLANGroupFilterSet(NameSlugSearchFilterSet):
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -437,23 +422,21 @@ class VLANGroupFilterSet(NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -508,7 +491,7 @@ class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ServiceFilterSet(CreatedUpdatedFilterSet):
|
||||
class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -1382,6 +1382,37 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ServiceCSVForm(CustomFieldModelCSVForm):
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of device',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found.',
|
||||
}
|
||||
)
|
||||
virtual_machine = FlexibleModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of virtual machine',
|
||||
error_messages={
|
||||
'invalid_choice': 'Virtual machine not found.',
|
||||
}
|
||||
)
|
||||
protocol = CSVChoiceField(
|
||||
choices=ServiceProtocolChoices,
|
||||
help_text='IP protocol'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = Service.csv_headers
|
||||
help_texts = {
|
||||
}
|
||||
|
||||
|
||||
class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Service.objects.all(),
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
|
||||
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'port', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ('protocol', 'port', 'pk') # (protocol, port) may be non-unique
|
||||
|
||||
@@ -53,11 +53,6 @@ class VRFTestCase(TestCase):
|
||||
params = {'enforce_unique': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
@@ -104,11 +99,6 @@ class RIRTestCase(TestCase):
|
||||
params = {'is_private': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class AggregateTestCase(TestCase):
|
||||
queryset = Aggregate.objects.all()
|
||||
@@ -265,11 +255,6 @@ class PrefixTestCase(TestCase):
|
||||
params = {'is_pool': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_within(self):
|
||||
params = {'within': '10.0.0.0/16'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@@ -425,11 +410,6 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': '10.0.0.0/24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@@ -640,11 +620,6 @@ class VLANTestCase(TestCase):
|
||||
params = {'vid': ['101', '201', '301']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:3]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
|
||||
@@ -333,9 +333,6 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Service
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_import_objects = None
|
||||
|
||||
# TODO: Resolve URL for Service creation
|
||||
test_create_object = None
|
||||
|
||||
@@ -365,6 +362,13 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,name,protocol,port,description",
|
||||
"Device 1,Service 1,TCP,1,First service",
|
||||
"Device 1,Service 2,TCP,2,Second service",
|
||||
"Device 1,Service 3,UDP,3,Third service",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'protocol': ServiceProtocolChoices.PROTOCOL_UDP,
|
||||
'port': 888,
|
||||
|
||||
@@ -94,6 +94,7 @@ urlpatterns = [
|
||||
|
||||
# Services
|
||||
path('services/', views.ServiceListView.as_view(), name='service_list'),
|
||||
path('services/import/', views.ServiceBulkImportView.as_view(), name='service_import'),
|
||||
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||
path('services/<int:pk>/', views.ServiceView.as_view(), name='service'),
|
||||
|
||||
@@ -1015,6 +1015,13 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
return service.parent.get_absolute_url()
|
||||
|
||||
|
||||
class ServiceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_service'
|
||||
model_form = forms.ServiceCSVForm
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
|
||||
class ServiceEditView(ServiceCreateView):
|
||||
permission_required = 'ipam.change_service'
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
DEVELOPER = getattr(configuration, 'DEVELOPER', False)
|
||||
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
|
||||
@@ -510,6 +511,7 @@ REST_FRAMEWORK = {
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
||||
'DEFAULT_FIELD_INSPECTORS': [
|
||||
'utilities.custom_inspectors.JSONFieldInspector',
|
||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||
'utilities.custom_inspectors.CustomChoiceFieldInspector',
|
||||
'utilities.custom_inspectors.TagListFieldInspector',
|
||||
@@ -525,7 +527,6 @@ SWAGGER_SETTINGS = {
|
||||
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
||||
],
|
||||
'DEFAULT_FILTER_INSPECTORS': [
|
||||
'utilities.custom_inspectors.IdInFilterInspector',
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
],
|
||||
'DEFAULT_INFO': 'netbox.urls.openapi_info',
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
|
||||
from netbox.views import APIRootView, HomeView, SearchView
|
||||
from netbox.views import APIRootView, HomeView, StaticMediaFailureView, SearchView
|
||||
from users.views import LoginView, LogoutView
|
||||
from .admin import admin_site
|
||||
|
||||
@@ -66,6 +66,9 @@ _patterns = [
|
||||
path('admin/', admin_site.urls),
|
||||
path('admin/webhook-backend-status/', include('django_rq.urls')),
|
||||
|
||||
# Errors
|
||||
path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),
|
||||
|
||||
]
|
||||
|
||||
# Plugins
|
||||
|
||||
@@ -300,6 +300,16 @@ class SearchView(View):
|
||||
})
|
||||
|
||||
|
||||
class StaticMediaFailureView(View):
|
||||
"""
|
||||
Display a user-friendly error message with troubleshooting tips when a static media file fails to load.
|
||||
"""
|
||||
def get(self, request):
|
||||
return render(request, 'media_failure.html', {
|
||||
'filename': request.GET.get('filename')
|
||||
})
|
||||
|
||||
|
||||
class APIRootView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
|
||||
@@ -234,6 +234,58 @@ table.report th a {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Admonition (docs) */
|
||||
.admonition {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.admonition p {
|
||||
padding: 0 12px;
|
||||
}
|
||||
.admonition pre {
|
||||
margin: 0 12px 10px;
|
||||
}
|
||||
.admonition p.admonition-title {
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: bold;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.admonition p.admonition-title::before {
|
||||
content: "\f06a";
|
||||
font-family: "FontAwesome";
|
||||
margin-right: 4px;
|
||||
}
|
||||
/* Admonition - Note */
|
||||
.admonition.note {
|
||||
background-color: rgb(231, 242, 250);
|
||||
}
|
||||
.admonition.note .admonition-title {
|
||||
background-color: rgb(106, 176, 222);
|
||||
}
|
||||
.admonition.note .admonition-title::before {
|
||||
content: "\f05a";
|
||||
}
|
||||
/* Admonition - Warning */
|
||||
.admonition.warning {
|
||||
background-color: rgb(255, 237, 204);
|
||||
}
|
||||
.admonition.warning .admonition-title {
|
||||
background-color: rgb(240, 179, 126);
|
||||
}
|
||||
.admonition.warning .admonition-title::before {
|
||||
content: "\f06a";
|
||||
}
|
||||
/* Admonition - Danger */
|
||||
.admonition.danger {
|
||||
background-color: rgb(253, 243, 242);
|
||||
}
|
||||
.admonition.danger .admonition-title {
|
||||
background-color: rgb(242, 159, 151);
|
||||
}
|
||||
.admonition.danger .admonition-title::before {
|
||||
content: "\f071";
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
.loading {
|
||||
position: fixed;
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
|
||||
from .models import Secret, SecretRole
|
||||
|
||||
|
||||
@@ -13,18 +13,14 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class SecretRoleFilterSet(NameSlugSearchFilterSet):
|
||||
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class SecretFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -72,11 +72,6 @@ class SecretTestCase(TestCase):
|
||||
params = {'name': ['Secret 1', 'Secret 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_role(self):
|
||||
roles = SecretRole.objects.all()[:2]
|
||||
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
||||
|
||||
@@ -206,7 +206,7 @@ class SecretBulkImportView(BulkImportView):
|
||||
|
||||
master_key = None
|
||||
|
||||
def _save_obj(self, obj_form):
|
||||
def _save_obj(self, obj_form, request):
|
||||
"""
|
||||
Encrypt each object before saving it to the database.
|
||||
"""
|
||||
|
||||
@@ -4,13 +4,27 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{% block title %}Home{% endblock %} - NetBox</title>
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-4.0.12/dist/css/select2.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'flatpickr-4.6.3/themes/light.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}?v{{ settings.VERSION }}">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=bootstrap-3.4.1-dist/css/bootstrap.min.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=font-awesome-4.7.0/css/font-awesome.min.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=jquery-ui-1.12.1/jquery-ui.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'select2-4.0.12/dist/css/select2.min.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=select2-4.0.12/dist/css/select2.min.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'flatpickr-4.6.3/themes/light.css' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=flatpickr-4.6.3/themes/light.css'">
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'css/base.css' %}?v{{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=css/base.css'">
|
||||
<link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
|
||||
@@ -66,13 +80,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="{% static 'jquery/jquery-3.4.1.min.js' %}"></script>
|
||||
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
|
||||
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'select2-4.0.12/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'clipboard.js/clipboard-2.0.4.min.js' %}"></script>
|
||||
<script src="{% static 'flatpickr-4.6.3/flatpickr.min.js' %}"></script>
|
||||
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script src="{% static 'jquery/jquery-3.4.1.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=jquery/jquery-3.4.1.min.js'"></script>
|
||||
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=jquery-ui-1.12.1/jquery-ui.min.js'"></script>
|
||||
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=bootstrap-3.4.1-dist/js/bootstrap.min.js'"></script>
|
||||
<script src="{% static 'select2-4.0.12/dist/js/select2.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=select2-4.0.12/dist/js/select2.min.js'"></script>
|
||||
<script src="{% static 'clipboard.js/clipboard-2.0.4.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=clipboard.js/clipboard-2.0.4.min.js'"></script>
|
||||
<script src="{% static 'flatpickr-4.6.3/flatpickr.min.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=flatpickr-4.6.3/flatpickr.min.js'"></script>
|
||||
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
var loading = $(".loading");
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if circuit.comments %}
|
||||
{{ circuit.comments|gfm }}
|
||||
{{ circuit.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -88,11 +88,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NOC Contact</td>
|
||||
<td class="rendered-markdown">{{ provider.noc_contact|gfm|placeholder }}</td>
|
||||
<td class="rendered-markdown">{{ provider.noc_contact|render_markdown|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Admin Contact</td>
|
||||
<td class="rendered-markdown">{{ provider.admin_contact|gfm|placeholder }}</td>
|
||||
<td class="rendered-markdown">{{ provider.admin_contact|render_markdown|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuits</td>
|
||||
@@ -110,7 +110,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if provider.comments %}
|
||||
{{ provider.comments|gfm }}
|
||||
{{ provider.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@@ -134,7 +134,7 @@
|
||||
{% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Type</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ termination_a|model_name|capfirst }}</p>
|
||||
<p class="form-control-static">{{ termination_a|meta:"verbose_name"|capfirst }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -327,7 +327,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if device.comments %}
|
||||
{{ device.comments|gfm }}
|
||||
{{ device.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@@ -875,7 +875,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
||||
{% include 'secrets/inc/private_key_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if devicetype.comments %}
|
||||
{{ devicetype.comments|gfm }}
|
||||
{{ devicetype.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
{{ termination|model_name|capfirst }}
|
||||
{{ termination|meta:"verbose_name"|capfirst }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="panel-body text-center">
|
||||
{% if end.device %}
|
||||
{# Device component #}
|
||||
{% with model=end|model_name %}
|
||||
{% with model=end|meta:"verbose_name" %}
|
||||
<strong>{{ model|bettertitle }} {{ end }}</strong><br />
|
||||
{% if model == 'interface' %}
|
||||
{{ end.get_type_display }}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if powerfeed.comments %}
|
||||
{{ powerfeed.comments|gfm }}
|
||||
{{ powerfeed.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if rack.comments %}
|
||||
{{ rack.comments|gfm }}
|
||||
{{ rack.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<td>
|
||||
{% if site.physical_address %}
|
||||
<div class="pull-right noprint">
|
||||
<a href="http://maps.google.com/?q={{ site.physical_address|oneline|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<a href="http://maps.google.com/?q={{ site.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
|
||||
<i class="glyphicon glyphicon-map-marker"></i> Map it
|
||||
</a>
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if site.comments %}
|
||||
{{ site.comments|gfm }}
|
||||
{{ site.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@@ -290,7 +290,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||
{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{% log_level level %}</td>
|
||||
<td class="rendered-markdown">{{ message|gfm }}</td>
|
||||
<td class="rendered-markdown">{{ message|render_markdown }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if tag.comments %}
|
||||
{{ tag.comments|gfm }}
|
||||
{{ tag.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<div class="modal fade" id="{{ modal_name }}_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" id="{{ name }}_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
{% if title %}
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">{{ title }}</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="{{ modal_name }}_modal_title">Modal title</h4>
|
||||
{{ content }}
|
||||
</div>
|
||||
<div class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,12 +67,17 @@
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:rackreservation_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Tenancy</li>
|
||||
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
|
||||
@@ -338,6 +343,11 @@
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Services</li>
|
||||
<li{% if not perms.ipam.view_service %} class="disabled"{% endif %}>
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'ipam:service_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'ipam:service_list' %}">Services</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">{{ obj.interface.parent|model_name|bettertitle }}</label>
|
||||
<label class="col-md-3 control-label">{{ obj.interface.parent|meta:"verbose_name"|bettertitle }}</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ obj.interface.parent.get_absolute_url }}">{{ obj.interface.parent }}</a>
|
||||
|
||||
48
netbox/templates/media_failure.html
Normal file
48
netbox/templates/media_failure.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Static Media Failure - NetBox</title>
|
||||
<meta charset="UTF-8">
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
li.tip {
|
||||
line-height: 150%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin: auto; width: 800px">
|
||||
<h1>Static Media Failure</h1>
|
||||
<h3>
|
||||
The following static media file failed to load:
|
||||
<a href="{% static filename %}"><code style="color: red">{{ filename }}</code></a>
|
||||
</h3>
|
||||
<p>Check the following:</p>
|
||||
<ul>
|
||||
<li class="tip">
|
||||
<code><strong>manage.py collectstatic</strong></code> was run during the most recent upgrade. This installs the most recent
|
||||
iteration of each static file into the static root path.
|
||||
</li>
|
||||
<li class="tip">
|
||||
The HTTP service (e.g. nginx or Apache) is configured to serve files from the <code>STATIC_ROOT</code> path.
|
||||
Refer to <a href="https://netbox.readthedocs.io/en/stable/installation/">the installation
|
||||
documentation</a> for further guidance.
|
||||
<ul>
|
||||
{% if request.user.is_staff or request.user.is_superuser %}
|
||||
<li><code>STATIC_ROOT: <strong>{{ settings.STATIC_ROOT }}</strong></code></li>
|
||||
{% endif %}
|
||||
<li><code>STATIC_URL: <strong>{{ settings.STATIC_URL }}</strong></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="tip">
|
||||
The file <code>{{ filename }}</code> exists in the static root directory and is readable by the HTTP process.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Click <a href="/">here</a> to attempt loading NetBox again.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if tenant.comments %}
|
||||
{{ tenant.comments|gfm }}
|
||||
{{ tenant.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
|
||||
@@ -9,7 +10,14 @@
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>{% block title %}{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}{% endblock %}</h3>
|
||||
<h3>
|
||||
{% if settings.DOCS_ROOT %}
|
||||
<div class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#docs_modal"><i class="fa fa-question"></i></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block title %}{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}{% endblock %}
|
||||
</h3>
|
||||
{% block tabs %}{% endblock %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
@@ -43,4 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if settings.DOCS_ROOT %}
|
||||
{% include 'inc/modal.html' with name='docs' content=obj|get_docs %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% export_button content_type %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ content_type.model_class|model_name_plural|bettertitle }}{% endblock %}</h1>
|
||||
<h1>{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-{% if filter_form %}9{% else %}12{% endif %}">
|
||||
{% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %}
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if cluster.comments %}
|
||||
{{ cluster.comments|gfm }}
|
||||
{{ cluster.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
</div>
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if virtualmachine.comments %}
|
||||
{{ virtualmachine.comments|gfm }}
|
||||
{{ virtualmachine.comments|render_markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -13,18 +13,14 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class TenantGroupFilterSet(NameSlugSearchFilterSet):
|
||||
class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class TenantFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class TenantFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -61,11 +61,6 @@ class TenantTestCase(TestCase):
|
||||
params = {'slug': ['tenant-1', 'tenant-2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_group(self):
|
||||
group = TenantGroup.objects.all()[:2]
|
||||
params = {'group_id': [group[0].pk, group[1].pk]}
|
||||
|
||||
@@ -28,12 +28,47 @@ COLOR_CHOICES = (
|
||||
('ffffff', 'White'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Filter lookup expressions
|
||||
#
|
||||
|
||||
FILTER_CHAR_BASED_LOOKUP_MAP = dict(
|
||||
n='exact',
|
||||
ic='icontains',
|
||||
nic='icontains',
|
||||
iew='iendswith',
|
||||
niew='iendswith',
|
||||
isw='istartswith',
|
||||
nisw='istartswith',
|
||||
ie='iexact',
|
||||
nie='iexact'
|
||||
)
|
||||
|
||||
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
|
||||
n='exact',
|
||||
lte='lte',
|
||||
lt='lt',
|
||||
gte='gte',
|
||||
gt='gt'
|
||||
)
|
||||
|
||||
FILTER_NEGATION_LOOKUP_MAP = dict(
|
||||
n='exact'
|
||||
)
|
||||
|
||||
FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
|
||||
n='in'
|
||||
)
|
||||
|
||||
|
||||
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
|
||||
# the advisory_lock contextmanager. When a lock is acquired,
|
||||
# one of these keys will be used to identify said lock.
|
||||
#
|
||||
# When adding a new key, pick something arbitrary and unique so
|
||||
# that it is easily searchable in query logs.
|
||||
|
||||
ADVISORY_LOCK_KEYS = {
|
||||
'available-prefixes': 100100,
|
||||
'available-ips': 100200,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema
|
||||
from drf_yasg.utils import get_serializer_ref_name
|
||||
@@ -75,26 +76,28 @@ class CustomChoiceFieldInspector(FieldInspector):
|
||||
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
|
||||
if isinstance(field, ChoiceField):
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_STRING)
|
||||
choices = field._choices
|
||||
choice_value = list(choices.keys())
|
||||
choice_label = list(choices.values())
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
|
||||
|
||||
choices = list(field._choices.keys())
|
||||
if set([None] + choices) == {None, True, False}:
|
||||
if set([None] + choice_value) == {None, True, False}:
|
||||
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
|
||||
# differentiated since they each have subtly different values in their choice keys.
|
||||
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
|
||||
# - face is an integer set {0, 1} which is easily confused with {False, True}
|
||||
schema_type = openapi.TYPE_STRING
|
||||
if all(type(x) == bool for x in [c for c in choices if c is not None]):
|
||||
if all(type(x) == bool for x in [c for c in choice_value if c is not None]):
|
||||
schema_type = openapi.TYPE_BOOLEAN
|
||||
value_schema = openapi.Schema(type=schema_type)
|
||||
value_schema = openapi.Schema(type=schema_type, enum=choice_value)
|
||||
value_schema['x-nullable'] = True
|
||||
|
||||
if isinstance(choices[0], int):
|
||||
if isinstance(choice_value[0], int):
|
||||
# Change value_schema for IPAddressFamilyChoices, RackWidthChoices
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER, enum=choice_value)
|
||||
|
||||
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
|
||||
"label": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"label": openapi.Schema(type=openapi.TYPE_STRING, enum=choice_label),
|
||||
"value": value_schema
|
||||
})
|
||||
|
||||
@@ -119,13 +122,12 @@ class NullableBooleanFieldInspector(FieldInspector):
|
||||
return result
|
||||
|
||||
|
||||
class IdInFilterInspector(FilterInspector):
|
||||
class JSONFieldInspector(FieldInspector):
|
||||
"""Required because by default, Swagger sees a JSONField as a string and not dict
|
||||
"""
|
||||
def process_result(self, result, method_name, obj, **kwargs):
|
||||
if isinstance(result, list):
|
||||
params = [p for p in result if isinstance(p, openapi.Parameter) and p.name == 'id__in']
|
||||
for p in params:
|
||||
p.type = 'string'
|
||||
|
||||
if isinstance(result, openapi.Schema) and isinstance(obj, JSONField):
|
||||
result.type = 'dict'
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import django_filters
|
||||
from copy import deepcopy
|
||||
from dcim.forms import MACAddressField
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django_filters.utils import get_model_field, resolve_field
|
||||
|
||||
from extras.models import Tag
|
||||
from utilities.constants import (
|
||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
)
|
||||
|
||||
|
||||
def multivalue_field_factory(field_class):
|
||||
@@ -73,13 +80,6 @@ class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
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
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NullableCharFieldFilter(django_filters.CharFilter):
|
||||
"""
|
||||
Allow matching on null field values by passing a special string used to signify NULL.
|
||||
@@ -111,6 +111,165 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
# FilterSets
|
||||
#
|
||||
|
||||
class BaseFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
A base filterset which provides common functionaly to all NetBox filtersets
|
||||
"""
|
||||
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
FILTER_DEFAULTS.update({
|
||||
models.AutoField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.CharField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.DateField: {
|
||||
'filter_class': MultiValueDateFilter
|
||||
},
|
||||
models.DateTimeField: {
|
||||
'filter_class': MultiValueDateTimeFilter
|
||||
},
|
||||
models.DecimalField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.EmailField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.FloatField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.IntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveSmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.SlugField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.SmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.TimeField: {
|
||||
'filter_class': MultiValueTimeFilter
|
||||
},
|
||||
models.URLField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
MACAddressField: {
|
||||
'filter_class': MultiValueMACAddressFilter
|
||||
},
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _get_filter_lookup_dict(existing_filter):
|
||||
# Choose the lookup expression map based on the filter type
|
||||
if isinstance(existing_filter, (
|
||||
MultiValueDateFilter,
|
||||
MultiValueDateTimeFilter,
|
||||
MultiValueNumberFilter,
|
||||
MultiValueTimeFilter
|
||||
)):
|
||||
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)):
|
||||
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
||||
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.ModelChoiceFilter,
|
||||
django_filters.ModelMultipleChoiceFilter,
|
||||
TagFilter
|
||||
)) or existing_filter.extra.get('choices'):
|
||||
# These filter types support only negation
|
||||
lookup_map = FILTER_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.filters.CharFilter,
|
||||
django_filters.MultipleChoiceFilter,
|
||||
MultiValueCharFilter,
|
||||
MultiValueMACAddressFilter
|
||||
)):
|
||||
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
|
||||
|
||||
else:
|
||||
lookup_map = None
|
||||
|
||||
return lookup_map
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
"""
|
||||
Override filter generation to support dynamic lookup expressions for certain filter types.
|
||||
|
||||
For specific filter types, new filters are created based on defined lookup expressions in
|
||||
the form `<field_name>__<lookup_expr>`
|
||||
"""
|
||||
# TODO: once 3.6 is the minimum required version of python, change this to a bare super() call
|
||||
# We have to do it this way in py3.5 becuase of django_filters.FilterSet's use of a metaclass
|
||||
filters = super(django_filters.FilterSet, cls).get_filters()
|
||||
|
||||
new_filters = {}
|
||||
for existing_filter_name, existing_filter in filters.items():
|
||||
# Loop over existing filters to extract metadata by which to create new filters
|
||||
|
||||
# If the filter makes use of a custom filter method or lookup expression skip it
|
||||
# as we cannot sanely handle these cases in a generic mannor
|
||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
||||
continue
|
||||
|
||||
# Choose the lookup expression map based on the filter type
|
||||
lookup_map = cls._get_filter_lookup_dict(existing_filter)
|
||||
if lookup_map is None:
|
||||
# Do not augment this filter type with more lookup expressions
|
||||
continue
|
||||
|
||||
# Get properties of the existing filter for later use
|
||||
field_name = existing_filter.field_name
|
||||
field = get_model_field(cls._meta.model, field_name)
|
||||
|
||||
# Create new filters for each lookup expression in the map
|
||||
for lookup_name, lookup_expr in lookup_map.items():
|
||||
new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
|
||||
|
||||
try:
|
||||
if existing_filter_name in cls.declared_filters:
|
||||
# The filter field has been explicity defined on the filterset class so we must manually
|
||||
# create the new filter with the same type because there is no guarantee the defined type
|
||||
# is the same as the default type for the field
|
||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = type(existing_filter)(
|
||||
field_name=field_name,
|
||||
lookup_expr=lookup_expr,
|
||||
label=existing_filter.label,
|
||||
exclude=existing_filter.exclude,
|
||||
distinct=existing_filter.distinct,
|
||||
**existing_filter.extra
|
||||
)
|
||||
else:
|
||||
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
||||
# Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
except django_filters.exceptions.FieldLookupError:
|
||||
# The filter could not be created because the lookup expression is not supported on the field
|
||||
continue
|
||||
|
||||
if lookup_name.startswith('n'):
|
||||
# This is a negation filter which requires a queryset.exclude() clause
|
||||
# Of course setting the negation of the existing filter's exclude attribute handles both cases
|
||||
new_filter.exclude = not existing_filter.exclude
|
||||
|
||||
new_filters[new_filter_name] = new_filter
|
||||
|
||||
filters.update(new_filters)
|
||||
return filters
|
||||
|
||||
|
||||
class NameSlugSearchFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
||||
@@ -127,54 +286,3 @@ class NameSlugSearchFilterSet(django_filters.FilterSet):
|
||||
models.Q(name__icontains=value) |
|
||||
models.Q(slug__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Update default filters
|
||||
#
|
||||
|
||||
FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
|
||||
FILTER_DEFAULTS.update({
|
||||
models.AutoField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.CharField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.DateField: {
|
||||
'filter_class': MultiValueDateFilter
|
||||
},
|
||||
models.DateTimeField: {
|
||||
'filter_class': MultiValueDateTimeFilter
|
||||
},
|
||||
models.DecimalField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.EmailField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.FloatField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.IntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveSmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.SlugField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.SmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.TimeField: {
|
||||
'filter_class': MultiValueTimeFilter
|
||||
},
|
||||
models.URLField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
})
|
||||
|
||||
@@ -498,14 +498,14 @@ class ExpandableIPAddressField(forms.CharField):
|
||||
|
||||
class CommentField(forms.CharField):
|
||||
"""
|
||||
A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
|
||||
A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text.
|
||||
"""
|
||||
widget = forms.Textarea
|
||||
default_label = ''
|
||||
# TODO: Port GFM syntax cheat sheet to internal documentation
|
||||
# TODO: Port Markdown cheat sheet to internal documentation
|
||||
default_helptext = '<i class="fa fa-info-circle"></i> '\
|
||||
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
|
||||
'GitHub-Flavored Markdown</a> syntax is supported'
|
||||
'Markdown</a> syntax is supported'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
required = kwargs.pop('required', False)
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
|
||||
import yaml
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -19,15 +20,6 @@ register = template.Library()
|
||||
# Filters
|
||||
#
|
||||
|
||||
@register.filter()
|
||||
def oneline(value):
|
||||
"""
|
||||
Replace each line break with a single space
|
||||
"""
|
||||
value = value.replace('\r', '')
|
||||
return value.replace('\n', ' ')
|
||||
|
||||
|
||||
@register.filter()
|
||||
def placeholder(value):
|
||||
"""
|
||||
@@ -39,32 +31,16 @@ def placeholder(value):
|
||||
return mark_safe(placeholder)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def getlist(value, arg):
|
||||
"""
|
||||
Return all values of a QueryDict key
|
||||
"""
|
||||
return value.getlist(arg)
|
||||
|
||||
|
||||
@register.filter
|
||||
def getkey(value, key):
|
||||
"""
|
||||
Return a dictionary item specified by key
|
||||
"""
|
||||
return value[key]
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def gfm(value):
|
||||
def render_markdown(value):
|
||||
"""
|
||||
Render text as GitHub-Flavored Markdown
|
||||
Render text as Markdown
|
||||
"""
|
||||
# Strip HTML tags
|
||||
value = strip_tags(value)
|
||||
|
||||
# Render Markdown with GFM extension
|
||||
html = markdown(value, extensions=['mdx_gfm'])
|
||||
# Render Markdown
|
||||
html = markdown(value, extensions=['fenced_code'])
|
||||
|
||||
return mark_safe(html)
|
||||
|
||||
@@ -86,19 +62,12 @@ def render_yaml(value):
|
||||
|
||||
|
||||
@register.filter()
|
||||
def model_name(obj):
|
||||
def meta(obj, attr):
|
||||
"""
|
||||
Return the name of the model of the given object
|
||||
Return the specified Meta attribute of a model. This is needed because Django does not permit templates
|
||||
to access attributes which begin with an underscore (e.g. _meta).
|
||||
"""
|
||||
return obj._meta.verbose_name
|
||||
|
||||
|
||||
@register.filter()
|
||||
def model_name_plural(obj):
|
||||
"""
|
||||
Return the plural name of the model of the given object
|
||||
"""
|
||||
return obj._meta.verbose_name_plural
|
||||
return getattr(obj._meta, attr, '')
|
||||
|
||||
|
||||
@register.filter()
|
||||
@@ -116,14 +85,6 @@ def url_name(model, action):
|
||||
return None
|
||||
|
||||
|
||||
@register.filter()
|
||||
def contains(value, arg):
|
||||
"""
|
||||
Test whether a value contains any of a given set of strings. `arg` should be a comma-separated list of strings.
|
||||
"""
|
||||
return any(s in value for s in arg.split(','))
|
||||
|
||||
|
||||
@register.filter()
|
||||
def bettertitle(value):
|
||||
"""
|
||||
@@ -216,6 +177,30 @@ def percentage(x, y):
|
||||
return round(x / y * 100)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def get_docs(model):
|
||||
"""
|
||||
Render and return documentation for the specified model.
|
||||
"""
|
||||
path = '{}/models/{}/{}.md'.format(
|
||||
settings.DOCS_ROOT,
|
||||
model._meta.app_label,
|
||||
model._meta.model_name
|
||||
)
|
||||
try:
|
||||
with open(path) as docfile:
|
||||
content = docfile.read()
|
||||
except FileNotFoundError:
|
||||
return "Unable to load documentation, file not found: {}".format(path)
|
||||
except IOError:
|
||||
return "Unable to load documentation, error reading file: {}".format(path)
|
||||
|
||||
# Render Markdown with the admonition extension
|
||||
content = markdown(content, extensions=['admonition', 'fenced_code'])
|
||||
|
||||
return mark_safe(content)
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from mptt.fields import TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Region, Site
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from dcim.choices import *
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.filters import DeviceFilterSet, SiteFilterSet
|
||||
from dcim.models import (
|
||||
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
|
||||
)
|
||||
from extras.models import TaggedItem
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter,
|
||||
MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
|
||||
|
||||
class TreeNodeMultipleChoiceFilterTest(TestCase):
|
||||
@@ -60,3 +72,447 @@ class TreeNodeMultipleChoiceFilterTest(TestCase):
|
||||
self.assertEqual(qs.count(), 2)
|
||||
self.assertEqual(qs[0], self.site1)
|
||||
self.assertEqual(qs[1], self.site3)
|
||||
|
||||
|
||||
class DummyModel(models.Model):
|
||||
"""
|
||||
Dummy model used by BaseFilterSetTest for filter validation. Should never appear in a schema migration.
|
||||
"""
|
||||
charfield = models.CharField(
|
||||
max_length=10
|
||||
)
|
||||
choicefield = models.IntegerField(
|
||||
choices=(('A', 1), ('B', 2), ('C', 3))
|
||||
)
|
||||
datefield = models.DateField()
|
||||
datetimefield = models.DateTimeField()
|
||||
integerfield = models.IntegerField()
|
||||
macaddressfield = MACAddressField()
|
||||
timefield = models.TimeField()
|
||||
treeforeignkeyfield = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
|
||||
class BaseFilterSetTest(TestCase):
|
||||
"""
|
||||
Ensure that a BaseFilterSet automatically creates the expected set of filters for each filter type.
|
||||
"""
|
||||
class DummyFilterSet(BaseFilterSet):
|
||||
charfield = django_filters.CharFilter()
|
||||
macaddressfield = MACAddressFilter()
|
||||
modelchoicefield = django_filters.ModelChoiceFilter(
|
||||
field_name='integerfield', # We're pretending this is a ForeignKey field
|
||||
queryset=Site.objects.all()
|
||||
)
|
||||
modelmultiplechoicefield = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='integerfield', # We're pretending this is a ForeignKey field
|
||||
queryset=Site.objects.all()
|
||||
)
|
||||
multiplechoicefield = django_filters.MultipleChoiceFilter(
|
||||
field_name='choicefield'
|
||||
)
|
||||
multivaluecharfield = MultiValueCharFilter(
|
||||
field_name='charfield'
|
||||
)
|
||||
tagfield = TagFilter()
|
||||
treeforeignkeyfield = TreeNodeMultipleChoiceFilter(
|
||||
queryset=DummyModel.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DummyModel
|
||||
fields = (
|
||||
'charfield',
|
||||
'choicefield',
|
||||
'datefield',
|
||||
'datetimefield',
|
||||
'integerfield',
|
||||
'macaddressfield',
|
||||
'modelchoicefield',
|
||||
'modelmultiplechoicefield',
|
||||
'multiplechoicefield',
|
||||
'tagfield',
|
||||
'timefield',
|
||||
'treeforeignkeyfield',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.filters = cls.DummyFilterSet().filters
|
||||
|
||||
def test_char_filter(self):
|
||||
self.assertIsInstance(self.filters['charfield'], django_filters.CharFilter)
|
||||
self.assertEqual(self.filters['charfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['charfield'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['charfield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['charfield__ie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['charfield__ie'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__nie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['charfield__nie'].exclude, True)
|
||||
self.assertEqual(self.filters['charfield__ic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['charfield__ic'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__nic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['charfield__nic'].exclude, True)
|
||||
self.assertEqual(self.filters['charfield__isw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['charfield__isw'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__nisw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['charfield__nisw'].exclude, True)
|
||||
self.assertEqual(self.filters['charfield__iew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['charfield__iew'].exclude, False)
|
||||
self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['charfield__niew'].exclude, True)
|
||||
|
||||
def test_mac_address_filter(self):
|
||||
self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)
|
||||
self.assertEqual(self.filters['macaddressfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['macaddressfield'].exclude, False)
|
||||
self.assertEqual(self.filters['macaddressfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['macaddressfield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['macaddressfield__ie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['macaddressfield__ie'].exclude, False)
|
||||
self.assertEqual(self.filters['macaddressfield__nie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['macaddressfield__nie'].exclude, True)
|
||||
self.assertEqual(self.filters['macaddressfield__ic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['macaddressfield__ic'].exclude, False)
|
||||
self.assertEqual(self.filters['macaddressfield__nic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['macaddressfield__nic'].exclude, True)
|
||||
self.assertEqual(self.filters['macaddressfield__isw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['macaddressfield__isw'].exclude, False)
|
||||
self.assertEqual(self.filters['macaddressfield__nisw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['macaddressfield__nisw'].exclude, True)
|
||||
self.assertEqual(self.filters['macaddressfield__iew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
|
||||
self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
|
||||
|
||||
def test_model_choice_filter(self):
|
||||
self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
|
||||
self.assertEqual(self.filters['modelchoicefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['modelchoicefield'].exclude, False)
|
||||
self.assertEqual(self.filters['modelchoicefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['modelchoicefield__n'].exclude, True)
|
||||
|
||||
def test_model_multiple_choice_filter(self):
|
||||
self.assertIsInstance(self.filters['modelmultiplechoicefield'], django_filters.ModelMultipleChoiceFilter)
|
||||
self.assertEqual(self.filters['modelmultiplechoicefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['modelmultiplechoicefield'].exclude, False)
|
||||
self.assertEqual(self.filters['modelmultiplechoicefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['modelmultiplechoicefield__n'].exclude, True)
|
||||
|
||||
def test_multi_value_char_filter(self):
|
||||
self.assertIsInstance(self.filters['multivaluecharfield'], MultiValueCharFilter)
|
||||
self.assertEqual(self.filters['multivaluecharfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['multivaluecharfield'].exclude, False)
|
||||
self.assertEqual(self.filters['multivaluecharfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['multivaluecharfield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['multivaluecharfield__ie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['multivaluecharfield__ie'].exclude, False)
|
||||
self.assertEqual(self.filters['multivaluecharfield__nie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['multivaluecharfield__nie'].exclude, True)
|
||||
self.assertEqual(self.filters['multivaluecharfield__ic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['multivaluecharfield__ic'].exclude, False)
|
||||
self.assertEqual(self.filters['multivaluecharfield__nic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['multivaluecharfield__nic'].exclude, True)
|
||||
self.assertEqual(self.filters['multivaluecharfield__isw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['multivaluecharfield__isw'].exclude, False)
|
||||
self.assertEqual(self.filters['multivaluecharfield__nisw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['multivaluecharfield__nisw'].exclude, True)
|
||||
self.assertEqual(self.filters['multivaluecharfield__iew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
|
||||
self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
|
||||
|
||||
def test_multi_value_date_filter(self):
|
||||
self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
|
||||
self.assertEqual(self.filters['datefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['datefield'].exclude, False)
|
||||
self.assertEqual(self.filters['datefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['datefield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['datefield__lt'].lookup_expr, 'lt')
|
||||
self.assertEqual(self.filters['datefield__lt'].exclude, False)
|
||||
self.assertEqual(self.filters['datefield__lte'].lookup_expr, 'lte')
|
||||
self.assertEqual(self.filters['datefield__lte'].exclude, False)
|
||||
self.assertEqual(self.filters['datefield__gt'].lookup_expr, 'gt')
|
||||
self.assertEqual(self.filters['datefield__gt'].exclude, False)
|
||||
self.assertEqual(self.filters['datefield__gte'].lookup_expr, 'gte')
|
||||
self.assertEqual(self.filters['datefield__gte'].exclude, False)
|
||||
|
||||
def test_multi_value_datetime_filter(self):
|
||||
self.assertIsInstance(self.filters['datetimefield'], MultiValueDateTimeFilter)
|
||||
self.assertEqual(self.filters['datetimefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['datetimefield'].exclude, False)
|
||||
self.assertEqual(self.filters['datetimefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['datetimefield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['datetimefield__lt'].lookup_expr, 'lt')
|
||||
self.assertEqual(self.filters['datetimefield__lt'].exclude, False)
|
||||
self.assertEqual(self.filters['datetimefield__lte'].lookup_expr, 'lte')
|
||||
self.assertEqual(self.filters['datetimefield__lte'].exclude, False)
|
||||
self.assertEqual(self.filters['datetimefield__gt'].lookup_expr, 'gt')
|
||||
self.assertEqual(self.filters['datetimefield__gt'].exclude, False)
|
||||
self.assertEqual(self.filters['datetimefield__gte'].lookup_expr, 'gte')
|
||||
self.assertEqual(self.filters['datetimefield__gte'].exclude, False)
|
||||
|
||||
def test_multi_value_number_filter(self):
|
||||
self.assertIsInstance(self.filters['integerfield'], MultiValueNumberFilter)
|
||||
self.assertEqual(self.filters['integerfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['integerfield'].exclude, False)
|
||||
self.assertEqual(self.filters['integerfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['integerfield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['integerfield__lt'].lookup_expr, 'lt')
|
||||
self.assertEqual(self.filters['integerfield__lt'].exclude, False)
|
||||
self.assertEqual(self.filters['integerfield__lte'].lookup_expr, 'lte')
|
||||
self.assertEqual(self.filters['integerfield__lte'].exclude, False)
|
||||
self.assertEqual(self.filters['integerfield__gt'].lookup_expr, 'gt')
|
||||
self.assertEqual(self.filters['integerfield__gt'].exclude, False)
|
||||
self.assertEqual(self.filters['integerfield__gte'].lookup_expr, 'gte')
|
||||
self.assertEqual(self.filters['integerfield__gte'].exclude, False)
|
||||
|
||||
def test_multi_value_time_filter(self):
|
||||
self.assertIsInstance(self.filters['timefield'], MultiValueTimeFilter)
|
||||
self.assertEqual(self.filters['timefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['timefield'].exclude, False)
|
||||
self.assertEqual(self.filters['timefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['timefield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['timefield__lt'].lookup_expr, 'lt')
|
||||
self.assertEqual(self.filters['timefield__lt'].exclude, False)
|
||||
self.assertEqual(self.filters['timefield__lte'].lookup_expr, 'lte')
|
||||
self.assertEqual(self.filters['timefield__lte'].exclude, False)
|
||||
self.assertEqual(self.filters['timefield__gt'].lookup_expr, 'gt')
|
||||
self.assertEqual(self.filters['timefield__gt'].exclude, False)
|
||||
self.assertEqual(self.filters['timefield__gte'].lookup_expr, 'gte')
|
||||
self.assertEqual(self.filters['timefield__gte'].exclude, False)
|
||||
|
||||
def test_multiple_choice_filter(self):
|
||||
self.assertIsInstance(self.filters['multiplechoicefield'], django_filters.MultipleChoiceFilter)
|
||||
self.assertEqual(self.filters['multiplechoicefield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['multiplechoicefield'].exclude, False)
|
||||
self.assertEqual(self.filters['multiplechoicefield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['multiplechoicefield__n'].exclude, True)
|
||||
self.assertEqual(self.filters['multiplechoicefield__ie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['multiplechoicefield__ie'].exclude, False)
|
||||
self.assertEqual(self.filters['multiplechoicefield__nie'].lookup_expr, 'iexact')
|
||||
self.assertEqual(self.filters['multiplechoicefield__nie'].exclude, True)
|
||||
self.assertEqual(self.filters['multiplechoicefield__ic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['multiplechoicefield__ic'].exclude, False)
|
||||
self.assertEqual(self.filters['multiplechoicefield__nic'].lookup_expr, 'icontains')
|
||||
self.assertEqual(self.filters['multiplechoicefield__nic'].exclude, True)
|
||||
self.assertEqual(self.filters['multiplechoicefield__isw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['multiplechoicefield__isw'].exclude, False)
|
||||
self.assertEqual(self.filters['multiplechoicefield__nisw'].lookup_expr, 'istartswith')
|
||||
self.assertEqual(self.filters['multiplechoicefield__nisw'].exclude, True)
|
||||
self.assertEqual(self.filters['multiplechoicefield__iew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
|
||||
self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
|
||||
self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
|
||||
|
||||
def test_tag_filter(self):
|
||||
self.assertIsInstance(self.filters['tagfield'], TagFilter)
|
||||
self.assertEqual(self.filters['tagfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['tagfield'].exclude, False)
|
||||
self.assertEqual(self.filters['tagfield__n'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['tagfield__n'].exclude, True)
|
||||
|
||||
def test_tree_node_multiple_choice_filter(self):
|
||||
self.assertIsInstance(self.filters['treeforeignkeyfield'], TreeNodeMultipleChoiceFilter)
|
||||
# TODO: lookup_expr different for negation?
|
||||
self.assertEqual(self.filters['treeforeignkeyfield'].lookup_expr, 'exact')
|
||||
self.assertEqual(self.filters['treeforeignkeyfield'].exclude, False)
|
||||
self.assertEqual(self.filters['treeforeignkeyfield__n'].lookup_expr, 'in')
|
||||
self.assertEqual(self.filters['treeforeignkeyfield__n'].exclude, True)
|
||||
|
||||
|
||||
class DynamicFilterLookupExpressionTest(TestCase):
|
||||
"""
|
||||
Validate function of automatically generated filters using the Device model as an example.
|
||||
"""
|
||||
device_queryset = Device.objects.all()
|
||||
device_filterset = DeviceFilterSet
|
||||
site_queryset = Site.objects.all()
|
||||
site_filterset = SiteFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
manufacturers = (
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||
)
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
device_types = (
|
||||
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', is_full_depth=True),
|
||||
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', is_full_depth=True),
|
||||
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', is_full_depth=False),
|
||||
)
|
||||
DeviceType.objects.bulk_create(device_types)
|
||||
|
||||
device_roles = (
|
||||
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||
)
|
||||
DeviceRole.objects.bulk_create(device_roles)
|
||||
|
||||
platforms = (
|
||||
Platform(name='Platform 1', slug='platform-1'),
|
||||
Platform(name='Platform 2', slug='platform-2'),
|
||||
Platform(name='Platform 3', slug='platform-3'),
|
||||
)
|
||||
Platform.objects.bulk_create(platforms)
|
||||
|
||||
regions = (
|
||||
Region(name='Region 1', slug='region-1'),
|
||||
Region(name='Region 2', slug='region-2'),
|
||||
Region(name='Region 3', slug='region-3'),
|
||||
)
|
||||
for region in regions:
|
||||
region.save()
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='abc-site-1', region=regions[0], asn=65001),
|
||||
Site(name='Site 2', slug='def-site-2', region=regions[1], asn=65101),
|
||||
Site(name='Site 3', slug='ghi-site-3', region=regions[2], asn=65201),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
Rack(name='Rack 3', site=sites[2]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, local_context_data={"foo": 123}),
|
||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED),
|
||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
||||
Interface(device=devices[0], name='Interface 2', mac_address='aa-00-00-00-00-01'),
|
||||
Interface(device=devices[1], name='Interface 3', mac_address='00-00-00-00-00-02'),
|
||||
Interface(device=devices[1], name='Interface 4', mac_address='bb-00-00-00-00-02'),
|
||||
Interface(device=devices[2], name='Interface 5', mac_address='00-00-00-00-00-03'),
|
||||
Interface(device=devices[2], name='Interface 6', mac_address='cc-00-00-00-00-03'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
def test_site_name_negation(self):
|
||||
params = {'name__n': ['Site 1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_slug_icontains(self):
|
||||
params = {'slug__ic': ['-1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
|
||||
|
||||
def test_site_slug_icontains_negation(self):
|
||||
params = {'slug__nic': ['-1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_slug_startswith(self):
|
||||
params = {'slug__isw': ['abc']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
|
||||
|
||||
def test_site_slug_startswith_negation(self):
|
||||
params = {'slug__nisw': ['abc']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_slug_endswith(self):
|
||||
params = {'slug__iew': ['-1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
|
||||
|
||||
def test_site_slug_endswith_negation(self):
|
||||
params = {'slug__niew': ['-1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_asn_lt(self):
|
||||
params = {'asn__lt': [65101]}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
|
||||
|
||||
def test_site_asn_lte(self):
|
||||
params = {'asn__lte': [65101]}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_asn_gt(self):
|
||||
params = {'asn__lt': [65101]}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
|
||||
|
||||
def test_site_asn_gte(self):
|
||||
params = {'asn__gte': [65101]}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_region_negation(self):
|
||||
params = {'region__n': ['region-1']}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_site_region_id_negation(self):
|
||||
params = {'region_id__n': [Region.objects.first().pk]}
|
||||
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_name_eq(self):
|
||||
params = {'name': ['Device 1']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
def test_device_name_negation(self):
|
||||
params = {'name__n': ['Device 1']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_name_startswith(self):
|
||||
params = {'name__isw': ['Device']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 3)
|
||||
|
||||
def test_device_name_startswith_negation(self):
|
||||
params = {'name__nisw': ['Device 1']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_name_endswith(self):
|
||||
params = {'name__iew': [' 1']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
def test_device_name_endswith_negation(self):
|
||||
params = {'name__niew': [' 1']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_name_icontains(self):
|
||||
params = {'name__ic': [' 2']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
def test_device_name_icontains_negation(self):
|
||||
params = {'name__nic': [' ']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 0)
|
||||
|
||||
def test_device_mac_address_negation(self):
|
||||
params = {'mac_address__n': ['00-00-00-00-00-01', 'aa-00-00-00-00-01']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_mac_address_startswith(self):
|
||||
params = {'mac_address__isw': ['aa:']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
def test_device_mac_address_startswith_negation(self):
|
||||
params = {'mac_address__nisw': ['aa:']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_mac_address_endswith(self):
|
||||
params = {'mac_address__iew': [':02']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
def test_device_mac_address_endswith_negation(self):
|
||||
params = {'mac_address__niew': [':02']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_mac_address_icontains(self):
|
||||
params = {'mac_address__ic': ['aa:', 'bb']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
|
||||
|
||||
def test_device_mac_address_icontains_negation(self):
|
||||
params = {'mac_address__nic': ['aa:', 'bb']}
|
||||
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
|
||||
|
||||
@@ -33,14 +33,20 @@ class NaturalizationTestCase(TestCase):
|
||||
# IOS/JunOS-style
|
||||
('Gi', '9999999999999999Gi000000000000000000'),
|
||||
('Gi1', '9999999999999999Gi000001000000000000'),
|
||||
('Gi1.0', '9999999999999999Gi000001000000000000'),
|
||||
('Gi1.1', '9999999999999999Gi000001000000000001'),
|
||||
('Gi1:0', '9999999999999999Gi000001000000000000'),
|
||||
('Gi1:0.0', '9999999999999999Gi000001000000000000'),
|
||||
('Gi1:0.1', '9999999999999999Gi000001000000000001'),
|
||||
('Gi1:1', '9999999999999999Gi000001000001000000'),
|
||||
('Gi1:1.0', '9999999999999999Gi000001000001000000'),
|
||||
('Gi1:1.1', '9999999999999999Gi000001000001000001'),
|
||||
('Gi1/2', '0001999999999999Gi000002000000000000'),
|
||||
('Gi1/2/3', '0001000299999999Gi000003000000000000'),
|
||||
('Gi1/2/3/4', '0001000200039999Gi000004000000000000'),
|
||||
('Gi1/2/3/4/5', '0001000200030004Gi000005000000000000'),
|
||||
('Gi1/2/3/4/5:6', '0001000200030004Gi000005000006000000'),
|
||||
('Gi1/2/3/4/5:6.7', '0001000200030004Gi000005000006000007'),
|
||||
('Gi1:2', '9999999999999999Gi000001000002000000'),
|
||||
('Gi1:2.3', '9999999999999999Gi000001000002000003'),
|
||||
# Generic
|
||||
('Interface 1', '9999999999999999Interface 000001000000000000'),
|
||||
('Interface 1 (other)', '9999999999999999Interface 000001000000000000 (other)'),
|
||||
|
||||
@@ -566,7 +566,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
||||
|
||||
return ImportForm(*args, **kwargs)
|
||||
|
||||
def _save_obj(self, obj_form):
|
||||
def _save_obj(self, obj_form, request):
|
||||
"""
|
||||
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
|
||||
"""
|
||||
@@ -595,7 +595,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
||||
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
||||
obj_form = self.model_form(data)
|
||||
if obj_form.is_valid():
|
||||
obj = self._save_obj(obj_form)
|
||||
obj = self._save_obj(obj_form, request)
|
||||
new_objs.append(obj)
|
||||
else:
|
||||
for field, err in obj_form.errors.items():
|
||||
|
||||
@@ -4,9 +4,9 @@ from django.db.models import Q
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import (
|
||||
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@@ -20,37 +20,35 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ClusterTypeFilterSet(NameSlugSearchFilterSet):
|
||||
class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ClusterGroupFilterSet(NameSlugSearchFilterSet):
|
||||
class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ClusterFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region__in',
|
||||
field_name='site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -100,15 +98,12 @@ class ClusterFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
|
||||
|
||||
|
||||
class VirtualMachineFilterSet(
|
||||
BaseFilterSet,
|
||||
LocalConfigContextFilterSet,
|
||||
TenancyFilterSet,
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -145,12 +140,14 @@ class VirtualMachineFilterSet(
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='cluster__site__region__in',
|
||||
field_name='cluster__site__region',
|
||||
lookup_expr='in',
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='cluster__site__region__in',
|
||||
field_name='cluster__site__region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
@@ -204,7 +201,7 @@ class VirtualMachineFilterSet(
|
||||
)
|
||||
|
||||
|
||||
class InterfaceFilterSet(django_filters.FilterSet):
|
||||
class InterfaceFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -125,11 +125,6 @@ class ClusterTestCase(TestCase):
|
||||
params = {'name': ['Cluster 1', 'Cluster 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
@@ -280,11 +275,6 @@ class VirtualMachineTestCase(TestCase):
|
||||
params = {'disk': [1, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_id__in(self):
|
||||
id_list = self.queryset.values_list('id', flat=True)[:2]
|
||||
params = {'id__in': ','.join([str(id) for id in id_list])}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [VirtualMachineStatusChoices.STATUS_ACTIVE, VirtualMachineStatusChoices.STATUS_STAGED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
Reference in New Issue
Block a user