mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
Merge branch 'feature' into 5284-vlangroup-scope
This commit is contained in:
commit
10778f8479
@ -77,6 +77,7 @@ The ObjectChange model (which is used to record the creation, modification, and
|
|||||||
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
|
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
|
||||||
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
|
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
|
||||||
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
|
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
|
||||||
|
* [#5972](https://github.com/netbox-community/netbox/issues/5972) - Enable bulk editing for organizational models
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
@ -4,10 +4,8 @@ from circuits.choices import CircuitStatusChoices
|
|||||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer
|
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
|
||||||
from netbox.api import ChoiceField
|
from netbox.api import ChoiceField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer, WritableNestedSerializer
|
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -16,7 +14,7 @@ from .nested_serializers import *
|
|||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class ProviderSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -55,7 +53,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEnd
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class CircuitSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
|
@ -142,6 +142,20 @@ class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=CircuitType.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
@ -73,6 +73,10 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Circuit Type 6,circuit-type-6",
|
"Circuit Type 6,circuit-type-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'Foo',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
@ -23,6 +23,7 @@ urlpatterns = [
|
|||||||
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||||
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||||
|
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
|
||||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||||
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||||
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
||||||
|
@ -107,6 +107,15 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.CircuitTypeTable
|
table = tables.CircuitTypeTable
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = CircuitType.objects.annotate(
|
||||||
|
circuit_count=count_related(Circuit, 'type')
|
||||||
|
)
|
||||||
|
filterset = filters.CircuitTypeFilterSet
|
||||||
|
table = tables.CircuitTypeTable
|
||||||
|
form = forms.CircuitTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.annotate(
|
||||||
circuit_count=count_related(Circuit, 'type')
|
circuit_count=count_related(Circuit, 'type')
|
||||||
|
@ -7,13 +7,12 @@ from rest_framework.validators import UniqueTogetherValidator
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer
|
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NestedGroupModelSerializer, OrganizationalModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
@ -43,7 +42,7 @@ class CableTerminationSerializer(serializers.ModelSerializer):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ConnectedEndpointSerializer(CustomFieldModelSerializer):
|
class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
||||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
||||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
||||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
||||||
@ -101,7 +100,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class SiteSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
@ -155,7 +154,7 @@ class RackRoleSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class RackSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
@ -206,7 +205,7 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
occupied = serializers.BooleanField(read_only=True)
|
occupied = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class RackReservationSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer()
|
||||||
user = NestedUserSerializer()
|
user = NestedUserSerializer()
|
||||||
@ -271,7 +270,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class DeviceTypeSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||||
@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class DeviceSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
device_role = NestedDeviceRoleSerializer()
|
device_role = NestedDeviceRoleSerializer()
|
||||||
@ -506,7 +505,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
|||||||
method = serializers.DictField()
|
method = serializers.DictField()
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
#
|
||||||
|
# Device components
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -530,7 +533,7 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -554,7 +557,7 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -583,7 +586,7 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
@ -602,7 +605,7 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
@ -643,7 +646,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
|||||||
return super().validate(data)
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
|
class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
@ -668,7 +671,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'label']
|
fields = ['id', 'url', 'name', 'label']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
|
class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
@ -684,7 +687,7 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Cu
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class DeviceBaySerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
@ -701,7 +704,7 @@ class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
# Inventory items
|
# Inventory items
|
||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# Provide a default value to satisfy UniqueTogetherValidator
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
@ -721,7 +724,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class CableSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
termination_a_type = ContentTypeField(
|
termination_a_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||||
@ -851,7 +854,7 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class VirtualChassisSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
master = NestedDeviceSerializer(required=False)
|
master = NestedDeviceSerializer(required=False)
|
||||||
member_count = serializers.IntegerField(read_only=True)
|
member_count = serializers.IntegerField(read_only=True)
|
||||||
@ -865,7 +868,7 @@ class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerialize
|
|||||||
# Power panels
|
# Power panels
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class PowerPanelSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(
|
location = NestedLocationSerializer(
|
||||||
@ -880,12 +883,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
fields = ['id', 'url', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
fields = ['id', 'url', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedSerializer(
|
class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
TaggedObjectSerializer,
|
|
||||||
CableTerminationSerializer,
|
|
||||||
ConnectedEndpointSerializer,
|
|
||||||
CustomFieldModelSerializer
|
|
||||||
):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
power_panel = NestedPowerPanelSerializer()
|
power_panel = NestedPowerPanelSerializer()
|
||||||
rack = NestedRackSerializer(
|
rack = NestedRackSerializer(
|
||||||
|
@ -201,6 +201,24 @@ class RegionCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = Region.csv_headers
|
fields = Region.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RegionFilterForm(BootstrapMixin, forms.Form):
|
class RegionFilterForm(BootstrapMixin, forms.Form):
|
||||||
model = Site
|
model = Site
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
@ -240,6 +258,24 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = SiteGroup.csv_headers
|
fields = SiteGroup.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupFilterForm(BootstrapMixin, forms.Form):
|
class SiteGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
model = Site
|
model = Site
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
@ -480,6 +516,31 @@ class LocationCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = Location.csv_headers
|
fields = Location.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class LocationFilterForm(BootstrapMixin, forms.Form):
|
class LocationFilterForm(BootstrapMixin, forms.Form):
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -530,6 +591,25 @@ class RackRoleCSVForm(CustomFieldModelCSVForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=RackRole.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
color = forms.CharField(
|
||||||
|
max_length=6, # RGB color code
|
||||||
|
required=False,
|
||||||
|
widget=ColorSelect()
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['color', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
@ -1026,6 +1106,20 @@ class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = Manufacturer.csv_headers
|
fields = Manufacturer.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
@ -1822,6 +1916,30 @@ class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
color = forms.CharField(
|
||||||
|
max_length=6, # RGB color code
|
||||||
|
required=False,
|
||||||
|
widget=ColorSelect()
|
||||||
|
)
|
||||||
|
vm_role = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect,
|
||||||
|
label='VM role'
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['color', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
@ -1859,6 +1977,29 @@ class PlatformCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = Platform.csv_headers
|
fields = Platform.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
manufacturer = DynamicModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
napalm_driver = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
# TODO: Bulk edit support for napalm_args
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['manufacturer', 'napalm_driver', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
@ -10,13 +10,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Sum
|
from django.db.models import Count, Sum
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from mptt.models import TreeForeignKey
|
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.elevations import RackElevationSVG
|
from dcim.elevations import RackElevationSVG
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -27,7 +26,6 @@ from .power import PowerFeed
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Rack',
|
'Rack',
|
||||||
'Location',
|
|
||||||
'RackReservation',
|
'RackReservation',
|
||||||
'RackRole',
|
'RackRole',
|
||||||
)
|
)
|
||||||
@ -37,65 +35,6 @@ __all__ = (
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
|
||||||
class Location(NestedGroupModel):
|
|
||||||
"""
|
|
||||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
|
||||||
site, or a room within a building, for example.
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=100
|
|
||||||
)
|
|
||||||
slug = models.SlugField(
|
|
||||||
max_length=100
|
|
||||||
)
|
|
||||||
site = models.ForeignKey(
|
|
||||||
to='dcim.Site',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='locations'
|
|
||||||
)
|
|
||||||
parent = TreeForeignKey(
|
|
||||||
to='self',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='children',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
db_index=True
|
|
||||||
)
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=200,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['site', 'name']
|
|
||||||
unique_together = [
|
|
||||||
['site', 'name'],
|
|
||||||
['site', 'slug'],
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return "{}?location_id={}".format(reverse('dcim:rack_list'), self.pk)
|
|
||||||
|
|
||||||
def to_csv(self):
|
|
||||||
return (
|
|
||||||
self.site,
|
|
||||||
self.parent.name if self.parent else '',
|
|
||||||
self.name,
|
|
||||||
self.slug,
|
|
||||||
self.description,
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Parent Location (if any) must belong to the same Site
|
|
||||||
if self.parent and self.parent.site != self.site:
|
|
||||||
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
|
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class RackRole(OrganizationalModel):
|
class RackRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from mptt.models import TreeForeignKey
|
from mptt.models import TreeForeignKey
|
||||||
@ -13,6 +14,7 @@ from utilities.fields import NaturalOrderingField
|
|||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'Location',
|
||||||
'Region',
|
'Region',
|
||||||
'Site',
|
'Site',
|
||||||
'SiteGroup',
|
'SiteGroup',
|
||||||
@ -276,3 +278,66 @@ class Site(PrimaryModel):
|
|||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return SiteStatusChoices.CSS_CLASSES.get(self.status)
|
return SiteStatusChoices.CSS_CLASSES.get(self.status)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Locations
|
||||||
|
#
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
|
class Location(NestedGroupModel):
|
||||||
|
"""
|
||||||
|
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||||
|
site, or a room within a building, for example.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
slug = models.SlugField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='locations'
|
||||||
|
)
|
||||||
|
parent = TreeForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='children',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['site', 'name']
|
||||||
|
unique_together = [
|
||||||
|
['site', 'name'],
|
||||||
|
['site', 'slug'],
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return "{}?location_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.site,
|
||||||
|
self.parent.name if self.parent else '',
|
||||||
|
self.name,
|
||||||
|
self.slug,
|
||||||
|
self.description,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Parent Location (if any) must belong to the same Site
|
||||||
|
if self.parent and self.parent.site != self.site:
|
||||||
|
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
|
||||||
|
@ -59,6 +59,56 @@ class RegionTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class SiteGroupTestCase(TestCase):
|
||||||
|
queryset = SiteGroup.objects.all()
|
||||||
|
filterset = SiteGroupFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
sitegroups = (
|
||||||
|
SiteGroup(name='Site Group 1', slug='site-group-1', description='A'),
|
||||||
|
SiteGroup(name='Site Group 2', slug='site-group-2', description='B'),
|
||||||
|
SiteGroup(name='Site Group 3', slug='site-group-3', description='C'),
|
||||||
|
)
|
||||||
|
for sitegroup in sitegroups:
|
||||||
|
sitegroup.save()
|
||||||
|
|
||||||
|
child_sitegroups = (
|
||||||
|
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]),
|
||||||
|
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]),
|
||||||
|
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]),
|
||||||
|
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]),
|
||||||
|
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]),
|
||||||
|
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]),
|
||||||
|
)
|
||||||
|
for sitegroup in child_sitegroups:
|
||||||
|
sitegroup.save()
|
||||||
|
|
||||||
|
def test_id(self):
|
||||||
|
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Site Group 1', 'Site Group 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_slug(self):
|
||||||
|
params = {'slug': ['site-group-1', 'site-group-2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['A', 'B']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_parent(self):
|
||||||
|
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||||
|
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
|
||||||
class SiteTestCase(TestCase):
|
class SiteTestCase(TestCase):
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.all()
|
||||||
filterset = SiteFilterSet
|
filterset = SiteFilterSet
|
||||||
|
@ -57,6 +57,44 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Region 6,region-6,Sixth region",
|
"Region 6,region-6,Sixth region",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
|
model = SiteGroup
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
# Create three SiteGroups
|
||||||
|
sitegroups = (
|
||||||
|
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||||
|
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||||
|
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||||
|
)
|
||||||
|
for sitegroup in sitegroups:
|
||||||
|
sitegroup.save()
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Site Group X',
|
||||||
|
'slug': 'site-group-x',
|
||||||
|
'parent': sitegroups[2].pk,
|
||||||
|
'description': 'A new site group',
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,slug,description",
|
||||||
|
"Site Group 4,site-group-4,Fourth site group",
|
||||||
|
"Site Group 5,site-group-5,Fifth site group",
|
||||||
|
"Site Group 6,site-group-6,Sixth site group",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Site
|
model = Site
|
||||||
@ -157,6 +195,10 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Site 1,Location 6,location-6,Sixth location",
|
"Site 1,Location 6,location-6,Sixth location",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
@ -184,6 +226,11 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Rack Role 6,rack-role-6,0000ff",
|
"Rack Role 6,rack-role-6,0000ff",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'color': '00ff00',
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
@ -345,6 +392,10 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
|
"Manufacturer 6,manufacturer-6,Sixth manufacturer",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||||
# Blocked by absence of bulk import view for DeviceTypes
|
# Blocked by absence of bulk import view for DeviceTypes
|
||||||
@ -894,6 +945,11 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Device Role 6,device-role-6,0000ff",
|
"Device Role 6,device-role-6,0000ff",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'color': '00ff00',
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = Platform
|
model = Platform
|
||||||
@ -925,6 +981,11 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Platform 6,platform-6,Sixth platform",
|
"Platform 6,platform-6,Sixth platform",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'napalm_driver': 'ios',
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Device
|
model = Device
|
||||||
|
@ -12,6 +12,7 @@ urlpatterns = [
|
|||||||
path('regions/', views.RegionListView.as_view(), name='region_list'),
|
path('regions/', views.RegionListView.as_view(), name='region_list'),
|
||||||
path('regions/add/', views.RegionEditView.as_view(), name='region_add'),
|
path('regions/add/', views.RegionEditView.as_view(), name='region_add'),
|
||||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||||
|
path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'),
|
||||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||||
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
||||||
@ -21,6 +22,7 @@ urlpatterns = [
|
|||||||
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
|
path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'),
|
||||||
path('site-groups/add/', views.SiteGroupEditView.as_view(), name='sitegroup_add'),
|
path('site-groups/add/', views.SiteGroupEditView.as_view(), name='sitegroup_add'),
|
||||||
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
|
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
|
||||||
|
path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'),
|
||||||
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
|
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
|
||||||
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
|
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
|
||||||
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
|
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
|
||||||
@ -42,6 +44,7 @@ urlpatterns = [
|
|||||||
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
path('locations/', views.LocationListView.as_view(), name='location_list'),
|
||||||
path('locations/add/', views.LocationEditView.as_view(), name='location_add'),
|
path('locations/add/', views.LocationEditView.as_view(), name='location_add'),
|
||||||
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
|
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
|
||||||
|
path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'),
|
||||||
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
|
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
|
||||||
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
|
||||||
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
|
||||||
@ -51,6 +54,7 @@ urlpatterns = [
|
|||||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||||
path('rack-roles/add/', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
path('rack-roles/add/', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
||||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||||
|
path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
|
||||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||||
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
||||||
@ -84,6 +88,7 @@ urlpatterns = [
|
|||||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||||
|
path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
|
||||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||||
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||||
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||||
@ -168,6 +173,7 @@ urlpatterns = [
|
|||||||
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
||||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||||
|
path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
|
||||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||||
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||||
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||||
@ -177,6 +183,7 @@ urlpatterns = [
|
|||||||
path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
||||||
path('platforms/add/', views.PlatformEditView.as_view(), name='platform_add'),
|
path('platforms/add/', views.PlatformEditView.as_view(), name='platform_add'),
|
||||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||||
|
path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
|
||||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||||
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||||
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||||
|
@ -126,6 +126,19 @@ class RegionBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.RegionTable
|
table = tables.RegionTable
|
||||||
|
|
||||||
|
|
||||||
|
class RegionBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Region.objects.add_related_count(
|
||||||
|
Region.objects.all(),
|
||||||
|
Site,
|
||||||
|
'region',
|
||||||
|
'site_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
filterset = filters.RegionFilterSet
|
||||||
|
table = tables.RegionTable
|
||||||
|
form = forms.RegionBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Region.objects.add_related_count(
|
queryset = Region.objects.add_related_count(
|
||||||
Region.objects.all(),
|
Region.objects.all(),
|
||||||
@ -170,6 +183,19 @@ class SiteGroupBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.SiteGroupTable
|
table = tables.SiteGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class SiteGroupBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = SiteGroup.objects.add_related_count(
|
||||||
|
SiteGroup.objects.all(),
|
||||||
|
Site,
|
||||||
|
'group',
|
||||||
|
'site_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
filterset = filters.SiteGroupFilterSet
|
||||||
|
table = tables.SiteGroupTable
|
||||||
|
form = forms.SiteGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SiteGroup.objects.add_related_count(
|
queryset = SiteGroup.objects.add_related_count(
|
||||||
SiteGroup.objects.all(),
|
SiteGroup.objects.all(),
|
||||||
@ -279,6 +305,19 @@ class LocationBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.LocationTable
|
table = tables.LocationTable
|
||||||
|
|
||||||
|
|
||||||
|
class LocationBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Location.objects.add_related_count(
|
||||||
|
Location.objects.all(),
|
||||||
|
Rack,
|
||||||
|
'location',
|
||||||
|
'rack_count',
|
||||||
|
cumulative=True
|
||||||
|
).prefetch_related('site')
|
||||||
|
filterset = filters.LocationFilterSet
|
||||||
|
table = tables.LocationTable
|
||||||
|
form = forms.LocationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Location.objects.add_related_count(
|
queryset = Location.objects.add_related_count(
|
||||||
Location.objects.all(),
|
Location.objects.all(),
|
||||||
@ -317,6 +356,15 @@ class RackRoleBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.RackRoleTable
|
table = tables.RackRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class RackRoleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = RackRole.objects.annotate(
|
||||||
|
rack_count=count_related(Rack, 'role')
|
||||||
|
)
|
||||||
|
filterset = filters.RackRoleFilterSet
|
||||||
|
table = tables.RackRoleTable
|
||||||
|
form = forms.RackRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.annotate(
|
||||||
rack_count=count_related(Rack, 'role')
|
rack_count=count_related(Rack, 'role')
|
||||||
@ -534,6 +582,15 @@ class ManufacturerBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.ManufacturerTable
|
table = tables.ManufacturerTable
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Manufacturer.objects.annotate(
|
||||||
|
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||||
|
)
|
||||||
|
filterset = filters.ManufacturerFilterSet
|
||||||
|
table = tables.ManufacturerTable
|
||||||
|
form = forms.ManufacturerBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.annotate(
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||||
@ -975,6 +1032,13 @@ class DeviceRoleBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.DeviceRoleTable
|
table = tables.DeviceRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = DeviceRole.objects.all()
|
||||||
|
filterset = filters.DeviceRoleFilterSet
|
||||||
|
table = tables.DeviceRoleTable
|
||||||
|
form = forms.DeviceRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = DeviceRole.objects.all()
|
queryset = DeviceRole.objects.all()
|
||||||
table = tables.DeviceRoleTable
|
table = tables.DeviceRoleTable
|
||||||
@ -1007,6 +1071,13 @@ class PlatformBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.PlatformTable
|
table = tables.PlatformTable
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Platform.objects.all()
|
||||||
|
filterset = filters.PlatformFilterSet
|
||||||
|
table = tables.PlatformTable
|
||||||
|
form = forms.PlatformBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
table = tables.PlatformTable
|
table = tables.PlatformTable
|
||||||
|
@ -2,6 +2,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from extras import choices, models
|
from extras import choices, models
|
||||||
from netbox.api import ChoiceField, WritableNestedSerializer
|
from netbox.api import ChoiceField, WritableNestedSerializer
|
||||||
|
from netbox.api.serializers import NestedTagSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -11,7 +12,7 @@ __all__ = [
|
|||||||
'NestedExportTemplateSerializer',
|
'NestedExportTemplateSerializer',
|
||||||
'NestedImageAttachmentSerializer',
|
'NestedImageAttachmentSerializer',
|
||||||
'NestedJobResultSerializer',
|
'NestedJobResultSerializer',
|
||||||
'NestedTagSerializer',
|
'NestedTagSerializer', # Defined in netbox.api.serializers
|
||||||
'NestedWebhookSerializer',
|
'NestedWebhookSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -64,14 +65,6 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'image']
|
fields = ['id', 'url', 'name', 'image']
|
||||||
|
|
||||||
|
|
||||||
class NestedTagSerializer(WritableNestedSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Tag
|
|
||||||
fields = ['id', 'url', 'name', 'slug', 'color']
|
|
||||||
|
|
||||||
|
|
||||||
class NestedJobResultSerializer(serializers.ModelSerializer):
|
class NestedJobResultSerializer(serializers.ModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
|
||||||
status = ChoiceField(choices=choices.JobResultStatusChoices)
|
status = ChoiceField(choices=choices.JobResultStatusChoices)
|
||||||
|
@ -21,7 +21,6 @@ from virtualization.api.nested_serializers import NestedClusterGroupSerializer,
|
|||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextSerializer',
|
'ConfigContextSerializer',
|
||||||
'ContentTypeSerializer',
|
'ContentTypeSerializer',
|
||||||
@ -39,7 +38,6 @@ __all__ = (
|
|||||||
'ScriptOutputSerializer',
|
'ScriptOutputSerializer',
|
||||||
'ScriptSerializer',
|
'ScriptSerializer',
|
||||||
'TagSerializer',
|
'TagSerializer',
|
||||||
'TaggedObjectSerializer',
|
|
||||||
'WebhookSerializer',
|
'WebhookSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,38 +129,6 @@ class TagSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
|
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
|
||||||
|
|
||||||
|
|
||||||
class TaggedObjectSerializer(serializers.Serializer):
|
|
||||||
tags = NestedTagSerializer(many=True, required=False)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
tags = validated_data.pop('tags', None)
|
|
||||||
instance = super().create(validated_data)
|
|
||||||
|
|
||||||
if tags is not None:
|
|
||||||
return self._save_tags(instance, tags)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
tags = validated_data.pop('tags', None)
|
|
||||||
|
|
||||||
# Cache tags on instance for change logging
|
|
||||||
instance._tags = tags or []
|
|
||||||
|
|
||||||
instance = super().update(instance, validated_data)
|
|
||||||
|
|
||||||
if tags is not None:
|
|
||||||
return self._save_tags(instance, tags)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def _save_tags(self, instance, tags):
|
|
||||||
if tags:
|
|
||||||
instance.tags.set(*[t.name for t in tags])
|
|
||||||
else:
|
|
||||||
instance.tags.clear()
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
@ -6,13 +6,12 @@ from rest_framework import serializers
|
|||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer
|
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer
|
from netbox.api.serializers import OrganizationalModelSerializer
|
||||||
|
from netbox.api.serializers import PrimaryModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||||
@ -23,7 +22,7 @@ from .nested_serializers import *
|
|||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class VRFSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
import_targets = SerializedPKRelatedField(
|
import_targets = SerializedPKRelatedField(
|
||||||
@ -53,7 +52,7 @@ class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
# Route targets
|
# Route targets
|
||||||
#
|
#
|
||||||
|
|
||||||
class RouteTargetSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class RouteTargetSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
@ -80,7 +79,7 @@ class RIRSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class AggregateSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
rir = NestedRIRSerializer()
|
rir = NestedRIRSerializer()
|
||||||
@ -154,7 +153,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
return serializer(obj.scope, context=context).data
|
return serializer(obj.scope, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class VLANSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
||||||
@ -189,7 +188,7 @@ class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
|
|
||||||
class PrefixSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class PrefixSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
@ -259,7 +258,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
|||||||
# IP addresses
|
# IP addresses
|
||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class IPAddressSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
@ -317,7 +316,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class ServiceSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
||||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||||
|
@ -217,6 +217,24 @@ class RIRCSVForm(CustomFieldModelCSVForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RIRBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=RIR.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
is_private = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['is_private', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilterForm(BootstrapMixin, forms.Form):
|
class RIRFilterForm(BootstrapMixin, forms.Form):
|
||||||
is_private = forms.NullBooleanField(
|
is_private = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -351,6 +369,23 @@ class RoleCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = Role.csv_headers
|
fields = Role.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class RoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Role.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
@ -1223,6 +1258,24 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = VLANGroup.csv_headers
|
fields = VLANGroup.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=VLANGroup.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['site', 'description']
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
region = DynamicModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
|
@ -118,6 +118,10 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"RIR 6,rir-6,Sixth RIR",
|
"RIR 6,rir-6,Sixth RIR",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@ -187,6 +191,10 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Role 6,role-6,1000",
|
"Role 6,role-6,1000",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@ -332,6 +340,10 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"VLAN Group 6,vlan-group-6,Sixth VLAN group",
|
"VLAN Group 6,vlan-group-6,Sixth VLAN group",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
|
@ -33,6 +33,7 @@ urlpatterns = [
|
|||||||
path('rirs/', views.RIRListView.as_view(), name='rir_list'),
|
path('rirs/', views.RIRListView.as_view(), name='rir_list'),
|
||||||
path('rirs/add/', views.RIREditView.as_view(), name='rir_add'),
|
path('rirs/add/', views.RIREditView.as_view(), name='rir_add'),
|
||||||
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||||
|
path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'),
|
||||||
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||||
path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||||
path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
|
path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
|
||||||
@ -53,6 +54,7 @@ urlpatterns = [
|
|||||||
path('roles/', views.RoleListView.as_view(), name='role_list'),
|
path('roles/', views.RoleListView.as_view(), name='role_list'),
|
||||||
path('roles/add/', views.RoleEditView.as_view(), name='role_add'),
|
path('roles/add/', views.RoleEditView.as_view(), name='role_add'),
|
||||||
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||||
|
path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'),
|
||||||
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||||
path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||||
path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
|
path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
|
||||||
@ -88,6 +90,7 @@ urlpatterns = [
|
|||||||
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||||
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
||||||
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||||
|
path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'),
|
||||||
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||||
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||||
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
|
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
|
||||||
|
@ -164,6 +164,15 @@ class RIRBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.RIRTable
|
table = tables.RIRTable
|
||||||
|
|
||||||
|
|
||||||
|
class RIRBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = RIR.objects.annotate(
|
||||||
|
aggregate_count=count_related(Aggregate, 'rir')
|
||||||
|
)
|
||||||
|
filterset = filters.RIRFilterSet
|
||||||
|
table = tables.RIRTable
|
||||||
|
form = forms.RIRBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkDeleteView(generic.BulkDeleteView):
|
class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
aggregate_count=count_related(Aggregate, 'rir')
|
aggregate_count=count_related(Aggregate, 'rir')
|
||||||
@ -298,6 +307,13 @@ class RoleBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.RoleTable
|
table = tables.RoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class RoleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Role.objects.all()
|
||||||
|
filterset = filters.RoleFilterSet
|
||||||
|
table = tables.RoleTable
|
||||||
|
form = forms.RoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkDeleteView(generic.BulkDeleteView):
|
class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Role.objects.all()
|
queryset = Role.objects.all()
|
||||||
table = tables.RoleTable
|
table = tables.RoleTable
|
||||||
@ -655,6 +671,15 @@ class VLANGroupBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.VLANGroupTable
|
table = tables.VLANGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
||||||
|
vlan_count=count_related(VLAN, 'group')
|
||||||
|
)
|
||||||
|
filterset = filters.VLANGroupFilterSet
|
||||||
|
table = tables.VLANGroupTable
|
||||||
|
form = forms.VLANGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLANGroup.objects.annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
|
@ -6,7 +6,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.fields import CreateOnlyDefault
|
from rest_framework.fields import CreateOnlyDefault
|
||||||
|
|
||||||
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField, Tag
|
||||||
from utilities.utils import dict_to_filter_params
|
from utilities.utils import dict_to_filter_params
|
||||||
|
|
||||||
|
|
||||||
@ -70,19 +70,14 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
|||||||
instance.custom_fields[field.name] = instance.cf.get(field.name)
|
instance.custom_fields[field.name] = instance.cf.get(field.name)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
#
|
||||||
pass
|
# Nested serializers
|
||||||
|
#
|
||||||
|
|
||||||
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
|
||||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
class WritableNestedSerializer(serializers.ModelSerializer):
|
class WritableNestedSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Returns a nested representation of an object on read, but accepts only a primary key on write.
|
Returns a nested representation of an object on read, but accepts only a primary key on write.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
@ -128,5 +123,71 @@ class WritableNestedSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Nested tags serialization
|
||||||
|
#
|
||||||
|
|
||||||
|
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
|
||||||
|
class NestedTagSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ['id', 'url', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Base model serializers
|
||||||
|
#
|
||||||
|
|
||||||
|
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
||||||
|
"""
|
||||||
|
Adds support for custom fields.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
||||||
|
"""
|
||||||
|
Adds support for custom fields and tags.
|
||||||
|
"""
|
||||||
|
tags = NestedTagSerializer(many=True, required=False)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
tags = validated_data.pop('tags', None)
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
|
return self._save_tags(instance, tags)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
tags = validated_data.pop('tags', None)
|
||||||
|
|
||||||
|
# Cache tags on instance for change logging
|
||||||
|
instance._tags = tags or []
|
||||||
|
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
|
return self._save_tags(instance, tags)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def _save_tags(self, instance, tags):
|
||||||
|
if tags:
|
||||||
|
instance.tags.set(*[t.name for t in tags])
|
||||||
|
else:
|
||||||
|
instance.tags.clear()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
||||||
|
"""
|
||||||
|
Extends OrganizationalModelSerializer to include MPTT support.
|
||||||
|
"""
|
||||||
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class BulkOperationSerializer(serializers.Serializer):
|
class BulkOperationSerializer(serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
|
@ -2,11 +2,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer
|
from netbox.api import ContentTypeField
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
|
||||||
from secrets.constants import SECRET_ASSIGNMENT_MODELS
|
from secrets.constants import SECRET_ASSIGNMENT_MODELS
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
from netbox.api import ContentTypeField, ValidatedModelSerializer
|
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ from .nested_serializers import *
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretRoleSerializer(CustomFieldModelSerializer):
|
class SecretRoleSerializer(OrganizationalModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
|
||||||
secret_count = serializers.IntegerField(read_only=True)
|
secret_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ class SecretRoleSerializer(CustomFieldModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SecretSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class SecretSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
|
||||||
assigned_object_type = ContentTypeField(
|
assigned_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(SECRET_ASSIGNMENT_MODELS)
|
queryset=ContentType.objects.filter(SECRET_ASSIGNMENT_MODELS)
|
||||||
|
@ -59,6 +59,20 @@ class SecretRoleCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = SecretRole.csv_headers
|
fields = SecretRole.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class SecretRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=SecretRole.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
@ -34,6 +34,10 @@ class SecretRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Secret Role 6,secret-role-6",
|
"Secret Role 6,secret-role-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||||
class SecretTestCase(
|
class SecretTestCase(
|
||||||
|
@ -11,6 +11,7 @@ urlpatterns = [
|
|||||||
path('secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
path('secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||||
path('secret-roles/add/', views.SecretRoleEditView.as_view(), name='secretrole_add'),
|
path('secret-roles/add/', views.SecretRoleEditView.as_view(), name='secretrole_add'),
|
||||||
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||||
|
path('secret-roles/edit/', views.SecretRoleBulkEditView.as_view(), name='secretrole_bulk_edit'),
|
||||||
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||||
path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||||
path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
|
path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
|
||||||
|
@ -48,6 +48,15 @@ class SecretRoleBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.SecretRoleTable
|
table = tables.SecretRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
class SecretRoleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = SecretRole.objects.annotate(
|
||||||
|
secret_count=count_related(Secret, 'role')
|
||||||
|
)
|
||||||
|
filterset = filters.SecretRoleFilterSet
|
||||||
|
table = tables.SecretRoleTable
|
||||||
|
form = forms.SecretRoleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleBulkDeleteView(generic.BulkDeleteView):
|
class SecretRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = SecretRole.objects.annotate(
|
queryset = SecretRole.objects.annotate(
|
||||||
secret_count=count_related(Secret, 'role')
|
secret_count=count_related(Secret, 'role')
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer, NestedGroupModelSerializer
|
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class TenantSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||||
group = NestedTenantGroupSerializer(required=False, allow_null=True)
|
group = NestedTenantGroupSerializer(required=False, allow_null=True)
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -44,6 +44,24 @@ class TenantGroupCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = TenantGroup.csv_headers
|
fields = TenantGroup.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tenants
|
# Tenants
|
||||||
#
|
#
|
||||||
|
@ -29,6 +29,10 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Tenant Group 6,tenant-group-6,Sixth tenant group",
|
"Tenant Group 6,tenant-group-6,Sixth tenant group",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Tenant
|
model = Tenant
|
||||||
|
@ -11,6 +11,7 @@ urlpatterns = [
|
|||||||
path('tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
|
path('tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
|
||||||
path('tenant-groups/add/', views.TenantGroupEditView.as_view(), name='tenantgroup_add'),
|
path('tenant-groups/add/', views.TenantGroupEditView.as_view(), name='tenantgroup_add'),
|
||||||
path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
|
path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
|
||||||
|
path('tenant-groups/edit/', views.TenantGroupBulkEditView.as_view(), name='tenantgroup_bulk_edit'),
|
||||||
path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
|
path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
|
||||||
path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
|
path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
|
||||||
path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'),
|
path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'),
|
||||||
|
@ -39,6 +39,19 @@ class TenantGroupBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.TenantGroupTable
|
table = tables.TenantGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class TenantGroupBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = TenantGroup.objects.add_related_count(
|
||||||
|
TenantGroup.objects.all(),
|
||||||
|
Tenant,
|
||||||
|
'group',
|
||||||
|
'tenant_count',
|
||||||
|
cumulative=True
|
||||||
|
)
|
||||||
|
filterset = filters.TenantGroupFilterSet
|
||||||
|
table = tables.TenantGroupTable
|
||||||
|
form = forms.TenantGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class TenantGroupBulkDeleteView(generic.BulkDeleteView):
|
class TenantGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = TenantGroup.objects.add_related_count(
|
queryset = TenantGroup.objects.add_related_count(
|
||||||
TenantGroup.objects.all(),
|
TenantGroup.objects.all(),
|
||||||
|
@ -1024,6 +1024,7 @@ class ViewTestCases:
|
|||||||
DeleteObjectViewTestCase,
|
DeleteObjectViewTestCase,
|
||||||
ListObjectsViewTestCase,
|
ListObjectsViewTestCase,
|
||||||
BulkImportObjectsViewTestCase,
|
BulkImportObjectsViewTestCase,
|
||||||
|
BulkEditObjectsViewTestCase,
|
||||||
BulkDeleteObjectsViewTestCase,
|
BulkDeleteObjectsViewTestCase,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -3,12 +3,10 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from netbox.api.serializers import CustomFieldModelSerializer
|
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer, ValidatedModelSerializer
|
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -41,7 +39,7 @@ class ClusterGroupSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class ClusterSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
type = NestedClusterTypeSerializer()
|
type = NestedClusterTypeSerializer()
|
||||||
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
||||||
@ -62,7 +60,7 @@ class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class VirtualMachineSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||||
site = NestedSiteSerializer(read_only=True)
|
site = NestedSiteSerializer(read_only=True)
|
||||||
@ -103,7 +101,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class VMInterfaceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class VMInterfaceSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
|
@ -46,6 +46,20 @@ class ClusterTypeCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = ClusterType.csv_headers
|
fields = ClusterType.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ClusterType.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cluster groups
|
# Cluster groups
|
||||||
#
|
#
|
||||||
@ -68,6 +82,20 @@ class ClusterGroupCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = ClusterGroup.csv_headers
|
fields = ClusterGroup.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
@ -33,6 +33,10 @@ class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Cluster Group 6,cluster-group-6,Sixth cluster group",
|
"Cluster Group 6,cluster-group-6,Sixth cluster group",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
@ -59,6 +63,10 @@ class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
"Cluster Type 6,cluster-type-6,Sixth cluster type",
|
"Cluster Type 6,cluster-type-6,Sixth cluster type",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
|
@ -12,6 +12,7 @@ urlpatterns = [
|
|||||||
path('cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'),
|
path('cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'),
|
||||||
path('cluster-types/add/', views.ClusterTypeEditView.as_view(), name='clustertype_add'),
|
path('cluster-types/add/', views.ClusterTypeEditView.as_view(), name='clustertype_add'),
|
||||||
path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
|
path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
|
||||||
|
path('cluster-types/edit/', views.ClusterTypeBulkEditView.as_view(), name='clustertype_bulk_edit'),
|
||||||
path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
|
path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
|
||||||
path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
|
path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
|
||||||
path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
|
path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
|
||||||
@ -21,6 +22,7 @@ urlpatterns = [
|
|||||||
path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
|
path('cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
|
||||||
path('cluster-groups/add/', views.ClusterGroupEditView.as_view(), name='clustergroup_add'),
|
path('cluster-groups/add/', views.ClusterGroupEditView.as_view(), name='clustergroup_add'),
|
||||||
path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
|
path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
|
||||||
|
path('cluster-groups/edit/', views.ClusterGroupBulkEditView.as_view(), name='clustergroup_bulk_edit'),
|
||||||
path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
|
path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
|
||||||
path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
|
path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
|
||||||
path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
|
path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
|
||||||
|
@ -42,6 +42,15 @@ class ClusterTypeBulkImportView(generic.BulkImportView):
|
|||||||
table = tables.ClusterTypeTable
|
table = tables.ClusterTypeTable
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterTypeBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = ClusterType.objects.annotate(
|
||||||
|
cluster_count=count_related(Cluster, 'type')
|
||||||
|
)
|
||||||
|
filterset = filters.ClusterTypeFilterSet
|
||||||
|
table = tables.ClusterTypeTable
|
||||||
|
form = forms.ClusterTypeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
|
class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ClusterType.objects.annotate(
|
queryset = ClusterType.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'type')
|
cluster_count=count_related(Cluster, 'type')
|
||||||
@ -70,11 +79,22 @@ class ClusterGroupDeleteView(generic.ObjectDeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterGroupBulkImportView(generic.BulkImportView):
|
class ClusterGroupBulkImportView(generic.BulkImportView):
|
||||||
queryset = ClusterGroup.objects.all()
|
queryset = ClusterGroup.objects.annotate(
|
||||||
|
cluster_count=count_related(Cluster, 'group')
|
||||||
|
)
|
||||||
model_form = forms.ClusterGroupCSVForm
|
model_form = forms.ClusterGroupCSVForm
|
||||||
table = tables.ClusterGroupTable
|
table = tables.ClusterGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterGroupBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = ClusterGroup.objects.annotate(
|
||||||
|
cluster_count=count_related(Cluster, 'group')
|
||||||
|
)
|
||||||
|
filterset = filters.ClusterGroupFilterSet
|
||||||
|
table = tables.ClusterGroupTable
|
||||||
|
form = forms.ClusterGroupBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
|
class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ClusterGroup.objects.annotate(
|
queryset = ClusterGroup.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'group')
|
cluster_count=count_related(Cluster, 'group')
|
||||||
|
Loading…
Reference in New Issue
Block a user