Jeremy Stretch 6 лет назад
Родитель
Сommit
d431efb7d4

+ 37 - 10
netbox/circuits/tests/test_views.py

@@ -36,6 +36,15 @@ class ProviderTestCase(StandardTestCases.Views):
             "Provider 6,provider-6",
         )
 
+        cls.bulk_edit_data = {
+            'asn': 65009,
+            'account': '5678',
+            'portal_url': 'http://example.com/portal2',
+            'noc_contact': 'noc2@example.com',
+            'admin_contact': 'admin2@example.com',
+            'comments': 'New comments',
+        }
+
 
 class CircuitTypeTestCase(StandardTestCases.Views):
     model = CircuitType
@@ -43,6 +52,7 @@ class CircuitTypeTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -73,23 +83,29 @@ class CircuitTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
-        provider.save()
+        providers = (
+            Provider(name='Provider 1', slug='provider-1', asn=65001),
+            Provider(name='Provider 2', slug='provider-2', asn=65002),
+        )
+        Provider.objects.bulk_create(providers)
 
-        circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
-        circuittype.save()
+        circuittypes = (
+            CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
+            CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
+        )
+        CircuitType.objects.bulk_create(circuittypes)
 
         Circuit.objects.bulk_create([
-            Circuit(cid='Circuit 1', provider=provider, type=circuittype),
-            Circuit(cid='Circuit 2', provider=provider, type=circuittype),
-            Circuit(cid='Circuit 3', provider=provider, type=circuittype),
+            Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
+            Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
+            Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
         ])
 
         cls.form_data = {
             'cid': 'Circuit X',
-            'provider': provider.pk,
-            'type': circuittype.pk,
-            'status': CircuitStatusChoices.STATUS_ACTIVE,
+            'provider': providers[1].pk,
+            'type': circuittypes[1].pk,
+            'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
             'tenant': None,
             'install_date': datetime.date(2020, 1, 1),
             'commit_rate': 1000,
@@ -104,3 +120,14 @@ class CircuitTestCase(StandardTestCases.Views):
             "Circuit 5,Provider 1,Circuit Type 1",
             "Circuit 6,Provider 1,Circuit Type 1",
         )
+
+        cls.bulk_edit_data = {
+            'provider': providers[1].pk,
+            'type': circuittypes[1].pk,
+            'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
+            'tenant': None,
+            'commit_rate': 2000,
+            'description': 'New description',
+            'comments': 'New comments',
+
+        }

+ 177 - 66
netbox/dcim/tests/test_views.py

@@ -19,6 +19,7 @@ class RegionTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -52,20 +53,24 @@ class SiteTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        region = Region(name='Region 1', slug='region-1')
-        region.save()
+        regions = (
+            Region(name='Region 1', slug='region-1'),
+            Region(name='Region 2', slug='region-2'),
+        )
+        for region in regions:
+            region.save()
 
         Site.objects.bulk_create([
-            Site(name='Site 1', slug='site-1', region=region),
-            Site(name='Site 2', slug='site-2', region=region),
-            Site(name='Site 3', slug='site-3', region=region),
+            Site(name='Site 1', slug='site-1', region=regions[0]),
+            Site(name='Site 2', slug='site-2', region=regions[0]),
+            Site(name='Site 3', slug='site-3', region=regions[0]),
         ])
 
         cls.form_data = {
             'name': 'Site X',
             'slug': 'site-x',
             'status': SiteStatusChoices.STATUS_PLANNED,
-            'region': region.pk,
+            'region': regions[1].pk,
             'tenant': None,
             'facility': 'Facility X',
             'asn': 65001,
@@ -89,6 +94,15 @@ class SiteTestCase(StandardTestCases.Views):
             "Site 6,site-6",
         )
 
+        cls.bulk_edit_data = {
+            'status': SiteStatusChoices.STATUS_PLANNED,
+            'region': regions[1].pk,
+            'tenant': None,
+            'asn': 65009,
+            'time_zone': pytz.timezone('US/Eastern'),
+            'description': 'New description',
+        }
+
 
 class RackGroupTestCase(StandardTestCases.Views):
     model = RackGroup
@@ -96,6 +110,7 @@ class RackGroupTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -129,6 +144,7 @@ class RackRoleTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -159,32 +175,40 @@ class RackReservationTestCase(StandardTestCases.Views):
 
     # Disable inapplicable tests
     test_get_object = None
-    test_create_object = None  # TODO: Fix URL name for view
+    test_create_object = None
+
+    # TODO: Fix URL name for view
     test_import_objects = None
 
     @classmethod
     def setUpTestData(cls):
 
-        user = User.objects.create_user(username='testuser2')
+        user2 = User.objects.create_user(username='testuser2')
+        user3 = User.objects.create_user(username='testuser3')
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
 
         rack = Rack(name='Rack 1', site=site)
         rack.save()
 
         RackReservation.objects.bulk_create([
-            RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
-            RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
-            RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
+            RackReservation(rack=rack, user=user2, units=[1, 2, 3], description='Reservation 1'),
+            RackReservation(rack=rack, user=user2, units=[4, 5, 6], description='Reservation 2'),
+            RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'),
         ])
 
         cls.form_data = {
             'rack': rack.pk,
             'units': [10, 11, 12],
-            'user': user.pk,
+            'user': user3.pk,
             'tenant': None,
-            'description': 'New reservation',
+            'description': 'Rack reservation',
+        }
+
+        cls.bulk_edit_data = {
+            'user': user3.pk,
+            'tenant': None,
+            'description': 'New description',
         }
 
 
@@ -194,24 +218,38 @@ class RackTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
-        rackgroup = RackGroup.objects.create(name='Rack Group 1', slug='rack-group-1', site=site)
-        rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
 
-        Rack.objects.bulk_create([
-            Rack(name='Rack 1', site=site),
-            Rack(name='Rack 2', site=site),
-            Rack(name='Rack 3', site=site),
-        ])
+        rackgroups = (
+            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
+            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
+        )
+        RackGroup.objects.bulk_create(rackgroups)
+
+        rackroles = (
+            RackRole(name='Rack Role 1', slug='rack-role-1'),
+            RackRole(name='Rack Role 2', slug='rack-role-2'),
+        )
+        RackRole.objects.bulk_create(rackroles)
+
+        Rack.objects.bulk_create((
+            Rack(name='Rack 1', site=sites[0]),
+            Rack(name='Rack 2', site=sites[0]),
+            Rack(name='Rack 3', site=sites[0]),
+        ))
 
         cls.form_data = {
             'name': 'Rack X',
             'facility_id': 'Facility X',
-            'site': site.pk,
-            'group': rackgroup.pk,
+            'site': sites[1].pk,
+            'group': rackgroups[1].pk,
             'tenant': None,
             'status': RackStatusChoices.STATUS_PLANNED,
-            'role': rackrole.pk,
+            'role': rackroles[1].pk,
             'serial': '123456',
             'asset_tag': 'ABCDEF',
             'type': RackTypeChoices.TYPE_CABINET,
@@ -232,6 +270,23 @@ class RackTestCase(StandardTestCases.Views):
             "Site 1,Rack 6,19,42",
         )
 
+        cls.bulk_edit_data = {
+            'site': sites[1].pk,
+            'group': rackgroups[1].pk,
+            'tenant': None,
+            'status': RackStatusChoices.STATUS_DEPRECATED,
+            'role': rackroles[1].pk,
+            'serial': '654321',
+            'type': RackTypeChoices.TYPE_4POST,
+            'width': RackWidthChoices.WIDTH_23IN,
+            'u_height': 49,
+            'desc_units': True,
+            'outer_width': 30,
+            'outer_depth': 30,
+            'outer_unit': RackDimensionUnitChoices.UNIT_INCH,
+            'comments': 'New comments',
+        }
+
 
 class ManufacturerTestCase(StandardTestCases.Views):
     model = Manufacturer
@@ -239,6 +294,7 @@ class ManufacturerTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -268,17 +324,20 @@ class DeviceTypeTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
+        manufacturers = (
+            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+            Manufacturer(name='Manufacturer 2', slug='manufacturer-2')
+        )
+        Manufacturer.objects.bulk_create(manufacturers)
 
         DeviceType.objects.bulk_create([
-            DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
-            DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
-            DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
+            DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]),
+            DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturers[0]),
+            DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]),
         ])
 
         cls.form_data = {
-            'manufacturer': manufacturer.pk,
+            'manufacturer': manufacturers[1].pk,
             'model': 'Device Type X',
             'slug': 'device-type-x',
             'part_number': '123ABC',
@@ -289,6 +348,12 @@ class DeviceTypeTestCase(StandardTestCases.Views):
             'tags': 'Alpha,Bravo,Charlie',
         }
 
+        cls.bulk_edit_data = {
+            'manufacturer': manufacturers[1].pk,
+            'u_height': 3,
+            'is_full_depth': False,
+        }
+
     def test_import_objects(self):
         """
         Custom import test for YAML-based imports (versus CSV)
@@ -451,6 +516,7 @@ class DeviceRoleTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -483,6 +549,7 @@ class PlatformTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -517,29 +584,54 @@ class DeviceTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
-        rack = Rack.objects.create(name='Rack 1', site=site)
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
+        racks = (
+            Rack(name='Rack 1', site=sites[0]),
+            Rack(name='Rack 2', site=sites[1]),
+        )
+        Rack.objects.bulk_create(racks)
+
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
-        devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
-        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
-        platform = Platform.objects.create(name='Platform 1', slug='platform-1')
+
+        devicetypes = (
+            DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
+            DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
+        )
+        DeviceType.objects.bulk_create(devicetypes)
+
+        deviceroles = (
+            DeviceRole(name='Device Role 1', slug='device-role-1'),
+            DeviceRole(name='Device Role 2', slug='device-role-2'),
+        )
+        DeviceRole.objects.bulk_create(deviceroles)
+
+        platforms = (
+            Platform(name='Platform 1', slug='platform-1'),
+            Platform(name='Platform 2', slug='platform-2'),
+        )
+        Platform.objects.bulk_create(platforms)
 
         Device.objects.bulk_create([
-            Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
-            Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
-            Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 1', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
+            Device(name='Device 2', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
+            Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
         ])
 
         cls.form_data = {
-            'device_type': devicetype.pk,
-            'device_role': devicerole.pk,
+            'device_type': devicetypes[1].pk,
+            'device_role': deviceroles[1].pk,
             'tenant': None,
-            'platform': platform.pk,
+            'platform': platforms[1].pk,
             'name': 'Device X',
             'serial': '123456',
             'asset_tag': 'ABCDEF',
-            'site': site.pk,
-            'rack': rack.pk,
+            'site': sites[1].pk,
+            'rack': racks[1].pk,
             'position': 1,
             'face': DeviceFaceChoices.FACE_FRONT,
             'status': DeviceStatusChoices.STATUS_PLANNED,
@@ -561,6 +653,15 @@ class DeviceTestCase(StandardTestCases.Views):
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6",
         )
 
+        cls.bulk_edit_data = {
+            'device_type': devicetypes[1].pk,
+            'device_role': deviceroles[1].pk,
+            'tenant': None,
+            'platform': platforms[1].pk,
+            'serial': '123456',
+            'status': DeviceStatusChoices.STATUS_DECOMMISSIONING,
+        }
+
 
 # TODO: Convert to StandardTestCases.Views
 class ConsolePortTestCase(TestCase):
@@ -1071,28 +1172,28 @@ class CableTestCase(StandardTestCases.Views):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
         devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
-        device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        device1.save()
-        device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
-        device2.save()
-        device3 = Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole)
-        device3.save()
-        device4 = Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole)
-        device4.save()
+
+        devices = (
+            Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole),
+        )
+        Device.objects.bulk_create(devices)
 
         interfaces = (
-            Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device3, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device3, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device3, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device4, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device4, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
-            Interface(device=device4, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[1], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[2], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[2], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
         )
         Interface.objects.bulk_create(interfaces)
 
@@ -1109,7 +1210,7 @@ class CableTestCase(StandardTestCases.Views):
             'termination_b_id': interfaces[3].pk,
             'type': CableTypeChoices.TYPE_CAT6,
             'status': CableStatusChoices.STATUS_PLANNED,
-            'label': 'New cable',
+            'label': 'Label',
             'color': 'c0c0c0',
             'length': 100,
             'length_unit': CableLengthUnitChoices.UNIT_FOOT,
@@ -1122,6 +1223,15 @@ class CableTestCase(StandardTestCases.Views):
             "Device 3,interface,Interface 3,Device 4,interface,Interface 3",
         )
 
+        cls.bulk_edit_data = {
+            'type': CableTypeChoices.TYPE_CAT5E,
+            'status': CableStatusChoices.STATUS_CONNECTED,
+            'label': 'New label',
+            'color': '00ff00',
+            'length': 50,
+            'length_unit': CableLengthUnitChoices.UNIT_METER,
+        }
+
 
 class VirtualChassisTestCase(StandardTestCases.Views):
     model = VirtualChassis
@@ -1129,6 +1239,7 @@ class VirtualChassisTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_import_objects = None
+    test_bulk_edit_objects = None
     test_bulk_delete_objects = None
 
     # TODO: Requires special form handling

+ 12 - 1
netbox/extras/tests/test_views.py

@@ -33,6 +33,10 @@ class TagTestCase(StandardTestCases.Views):
             'comments': 'Some comments',
         }
 
+        cls.bulk_edit_data = {
+            'color': '00ff00',
+        }
+
 
 class ConfigContextTestCase(StandardTestCases.Views):
     model = ConfigContext
@@ -53,7 +57,7 @@ class ConfigContextTestCase(StandardTestCases.Views):
         for i in range(1, 4):
             configcontext = ConfigContext(
                 name='Config Context {}'.format(i),
-                data='{{"foo": {}}}'.format(i)
+                data={'foo': i}
             )
             configcontext.save()
             configcontext.sites.add(site)
@@ -73,7 +77,14 @@ class ConfigContextTestCase(StandardTestCases.Views):
             'data': '{"foo": 123}',
         }
 
+        cls.bulk_edit_data = {
+            'weight': 300,
+            'is_active': False,
+            'description': 'New description',
+        }
+
 
+# TODO: Convert to StandardTestCases.Views
 class ObjectChangeTestCase(TestCase):
     user_permissions = (
         'extras.view_objectchange',

+ 111 - 29
netbox/ipam/tests/test_views.py

@@ -36,6 +36,12 @@ class VRFTestCase(StandardTestCases.Views):
             "VRF 6",
         )
 
+        cls.bulk_edit_data = {
+            'tenant': None,
+            'enforce_unique': False,
+            'description': 'New description',
+        }
+
 
 class RIRTestCase(StandardTestCases.Views):
     model = RIR
@@ -43,6 +49,7 @@ class RIRTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -73,18 +80,22 @@ class AggregateTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        rir = RIR.objects.create(name='RIR 1', slug='rir-1')
+        rirs = (
+            RIR(name='RIR 1', slug='rir-1'),
+            RIR(name='RIR 2', slug='rir-2'),
+        )
+        RIR.objects.bulk_create(rirs)
 
         Aggregate.objects.bulk_create([
-            Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
-            Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
-            Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
+            Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rirs[0]),
+            Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rirs[0]),
+            Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]),
         ])
 
         cls.form_data = {
             'family': 4,
             'prefix': IPNetwork('10.99.0.0/16'),
-            'rir': rir.pk,
+            'rir': rirs[1].pk,
             'date_added': datetime.date(2020, 1, 1),
             'description': 'A new aggregate',
             'tags': 'Alpha,Bravo,Charlie',
@@ -97,6 +108,12 @@ class AggregateTestCase(StandardTestCases.Views):
             "10.6.0.0/16,RIR 1",
         )
 
+        cls.bulk_edit_data = {
+            'rir': rirs[1].pk,
+            'date_added': datetime.date(2020, 1, 1),
+            'description': 'New description',
+        }
+
 
 class RoleTestCase(StandardTestCases.Views):
     model = Role
@@ -104,6 +121,7 @@ class RoleTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -135,25 +153,37 @@ class PrefixTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
-        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
-        role = Role.objects.create(name='Role 1', slug='role-1')
-        # vlan = VLAN.objects.create(vid=123, name='VLAN 123')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
+        vrfs = (
+            VRF(name='VRF 1', rd='65000:1'),
+            VRF(name='VRF 2', rd='65000:2'),
+        )
+        VRF.objects.bulk_create(vrfs)
+
+        roles = (
+            Role(name='Role 1', slug='role-1'),
+            Role(name='Role 2', slug='role-2'),
+        )
 
         Prefix.objects.bulk_create([
-            Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
-            Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
-            Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
+            Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
+            Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
+            Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
         ])
 
         cls.form_data = {
             'prefix': IPNetwork('192.0.2.0/24'),
-            'site': site.pk,
-            'vrf': vrf.pk,
+            'site': sites[1].pk,
+            'vrf': vrfs[1].pk,
             'tenant': None,
             'vlan': None,
             'status': PrefixStatusChoices.STATUS_RESERVED,
-            'role': role.pk,
+            'role': roles[1].pk,
             'is_pool': True,
             'description': 'A new prefix',
             'tags': 'Alpha,Bravo,Charlie',
@@ -166,6 +196,16 @@ class PrefixTestCase(StandardTestCases.Views):
             "10.6.0.0/16,Active",
         )
 
+        cls.bulk_edit_data = {
+            'site': sites[1].pk,
+            'vrf': vrfs[1].pk,
+            'tenant': None,
+            'status': PrefixStatusChoices.STATUS_RESERVED,
+            'role': roles[1].pk,
+            'is_pool': False,
+            'description': 'New description',
+        }
+
 
 class IPAddressTestCase(StandardTestCases.Views):
     model = IPAddress
@@ -173,16 +213,19 @@ class IPAddressTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
+        vrfs = (
+            VRF(name='VRF 1', rd='65000:1'),
+            VRF(name='VRF 2', rd='65000:2'),
+        )
 
         IPAddress.objects.bulk_create([
-            IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf),
-            IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrf),
-            IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf),
+            IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]),
+            IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrfs[0]),
+            IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]),
         ])
 
         cls.form_data = {
-            'vrf': vrf.pk,
+            'vrf': vrfs[1].pk,
             'address': IPNetwork('192.0.2.99/24'),
             'tenant': None,
             'status': IPAddressStatusChoices.STATUS_RESERVED,
@@ -201,6 +244,15 @@ class IPAddressTestCase(StandardTestCases.Views):
             "192.0.2.6/24,Active",
         )
 
+        cls.bulk_edit_data = {
+            'vrf': vrfs[1].pk,
+            'tenant': None,
+            'status': IPAddressStatusChoices.STATUS_RESERVED,
+            'role': IPAddressRoleChoices.ROLE_ANYCAST,
+            'dns_name': 'example',
+            'description': 'New description',
+        }
+
 
 class VLANGroupTestCase(StandardTestCases.Views):
     model = VLANGroup
@@ -208,6 +260,7 @@ class VLANGroupTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -240,24 +293,38 @@ class VLANTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
-        vlangroup = VLANGroup.objects.create(name='VLAN Group 1', slug='vlan-group-1', site=site)
-        role = Role.objects.create(name='Role 1', slug='role-1')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
+        vlangroups = (
+            VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
+            VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
+        )
+        VLANGroup.objects.bulk_create(vlangroups)
+
+        roles = (
+            Role(name='Role 1', slug='role-1'),
+            Role(name='Role 2', slug='role-2'),
+        )
+        Role.objects.bulk_create(roles)
 
         VLAN.objects.bulk_create([
-            VLAN(group=vlangroup, vid=101, name='VLAN101'),
-            VLAN(group=vlangroup, vid=102, name='VLAN102'),
-            VLAN(group=vlangroup, vid=103, name='VLAN103'),
+            VLAN(group=vlangroups[0], vid=101, name='VLAN101', site=sites[0], role=roles[0]),
+            VLAN(group=vlangroups[0], vid=102, name='VLAN102', site=sites[0], role=roles[0]),
+            VLAN(group=vlangroups[0], vid=103, name='VLAN103', site=sites[0], role=roles[0]),
         ])
 
         cls.form_data = {
-            'site': site.pk,
-            'group': vlangroup.pk,
+            'site': sites[1].pk,
+            'group': vlangroups[1].pk,
             'vid': 999,
             'name': 'VLAN999',
             'tenant': None,
             'status': VLANStatusChoices.STATUS_RESERVED,
-            'role': role.pk,
+            'role': roles[1].pk,
             'description': 'A new VLAN',
             'tags': 'Alpha,Bravo,Charlie',
         }
@@ -269,6 +336,15 @@ class VLANTestCase(StandardTestCases.Views):
             "106,VLAN106,Active",
         )
 
+        cls.bulk_edit_data = {
+            'site': sites[1].pk,
+            'group': vlangroups[1].pk,
+            'tenant': None,
+            'status': VLANStatusChoices.STATUS_RESERVED,
+            'role': roles[1].pk,
+            'description': 'New description',
+        }
+
 
 class ServiceTestCase(StandardTestCases.Views):
     model = Service
@@ -304,3 +380,9 @@ class ServiceTestCase(StandardTestCases.Views):
             'description': 'A new service',
             'tags': 'Alpha,Bravo,Charlie',
         }
+
+        cls.bulk_edit_data = {
+            'protocol': ServiceProtocolChoices.PROTOCOL_UDP,
+            'port': 888,
+            'description': 'New description',
+        }

+ 27 - 9
netbox/secrets/tests/test_views.py

@@ -14,6 +14,7 @@ class SecretRoleTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -56,21 +57,38 @@ class SecretTestCase(StandardTestCases.Views):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
         devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
-        device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        secretrole = SecretRole.objects.create(name='Secret Role 1', slug='secret-role-1')
 
-        Secret.objects.bulk_create([
-            Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
-            Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'),
-            Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
-        ])
+        devices = (
+            Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
+            Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
+        )
+        Device.objects.bulk_create(devices)
+
+        secretroles = (
+            SecretRole(name='Secret Role 1', slug='secret-role-1'),
+            SecretRole(name='Secret Role 2', slug='secret-role-2'),
+        )
+        SecretRole.objects.bulk_create(secretroles)
+
+        # Create one secret per device to allow bulk-editing of names (which must be unique per device/role)
+        Secret.objects.bulk_create((
+            Secret(device=devices[0], role=secretroles[0], name='Secret 1', ciphertext=b'1234567890'),
+            Secret(device=devices[1], role=secretroles[0], name='Secret 2', ciphertext=b'1234567890'),
+            Secret(device=devices[2], role=secretroles[0], name='Secret 3', ciphertext=b'1234567890'),
+        ))
 
         cls.form_data = {
-            'device': device.pk,
-            'role': secretrole.pk,
+            'device': devices[1].pk,
+            'role': secretroles[1].pk,
             'name': 'Secret X',
         }
 
+        cls.bulk_edit_data = {
+            'role': secretroles[1].pk,
+            'name': 'New name',
+        }
+
     def setUp(self):
 
         super().setUp()

+ 14 - 5
netbox/tenancy/tests/test_views.py

@@ -8,6 +8,7 @@ class TenantGroupTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -37,18 +38,22 @@ class TenantTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        tenantgroup = TenantGroup.objects.create(name='Tenant Group 1', slug='tenant-group-1')
+        tenantgroups = (
+            TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
+            TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
+        )
+        TenantGroup.objects.bulk_create(tenantgroups)
 
         Tenant.objects.bulk_create([
-            Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup),
-            Tenant(name='Tenant 2', slug='tenant-2', group=tenantgroup),
-            Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup),
+            Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroups[0]),
+            Tenant(name='Tenant 2', slug='tenant-2', group=tenantgroups[0]),
+            Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroups[0]),
         ])
 
         cls.form_data = {
             'name': 'Tenant X',
             'slug': 'tenant-x',
-            'group': tenantgroup.pk,
+            'group': tenantgroups[1].pk,
             'description': 'A new tenant',
             'comments': 'Some comments',
             'tags': 'Alpha,Bravo,Charlie',
@@ -60,3 +65,7 @@ class TenantTestCase(StandardTestCases.Views):
             "Tenant 5,tenant-5",
             "Tenant 6,tenant-6",
         )
+
+        cls.bulk_edit_data = {
+            'group': tenantgroups[1].pk,
+        }

+ 44 - 2
netbox/utilities/testing/testcases.py

@@ -85,8 +85,15 @@ class StandardTestCases:
             - Import multiple new objects
         """
         model = None
+
+        # Data to be sent when creating/editing individual objects
         form_data = {}
-        csv_data = {}
+
+        # CSV lines used for bulk import of new objects
+        csv_data = ()
+
+        # Form data to be used when editing multiple objects at once
+        bulk_edit_data = {}
 
         maxDiff = None
 
@@ -107,7 +114,7 @@ class StandardTestCases:
                 self.model._meta.model_name
             )
 
-            if action in ('list', 'add', 'import', 'bulk_delete'):
+            if action in ('list', 'add', 'import', 'bulk_edit', 'bulk_delete'):
                 return reverse(url_format.format(action))
 
             elif action in ('get', 'edit', 'delete'):
@@ -254,6 +261,41 @@ class StandardTestCases:
 
             self.assertEqual(self.model.objects.count(), initial_count + len(self.csv_data) - 1)
 
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_bulk_edit_objects(self):
+            pk_list = self.model.objects.values_list('pk', flat=True)
+
+            request = {
+                'path': self._get_url('bulk_edit'),
+                'data': {
+                    'pk': pk_list,
+                    '_apply': True,  # Form button
+                },
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Append the form data to the request
+            request['data'].update(post_data(self.bulk_edit_data))
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            bulk_edit_fields = self.bulk_edit_data.keys()
+            for i, instance in enumerate(self.model.objects.filter(pk__in=pk_list)):
+                self.assertDictEqual(
+                    model_to_dict(instance, fields=bulk_edit_fields),
+                    self.bulk_edit_data,
+                    msg="Instance {} failed to validate after bulk edit: {}".format(i, instance)
+                )
+
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_bulk_delete_objects(self):
             pk_list = self.model.objects.values_list('pk', flat=True)

+ 69 - 18
netbox/virtualization/tests/test_views.py

@@ -10,6 +10,7 @@ class ClusterGroupTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -39,6 +40,7 @@ class ClusterTypeTestCase(StandardTestCases.Views):
     # Disable inapplicable tests
     test_get_object = None
     test_delete_object = None
+    test_bulk_edit_objects = None
 
     @classmethod
     def setUpTestData(cls):
@@ -68,22 +70,36 @@ class ClusterTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        site = Site.objects.create(name='Site 1', slug='site-1')
-        clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
-        clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
+        clustergroups = (
+            ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
+            ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
+        )
+        ClusterGroup.objects.bulk_create(clustergroups)
+
+        clustertypes = (
+            ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
+            ClusterType(name='Cluster Type 2', slug='cluster-type-2'),
+        )
+        ClusterType.objects.bulk_create(clustertypes)
 
         Cluster.objects.bulk_create([
-            Cluster(name='Cluster 1', group=clustergroup, type=clustertype),
-            Cluster(name='Cluster 2', group=clustergroup, type=clustertype),
-            Cluster(name='Cluster 3', group=clustergroup, type=clustertype),
+            Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
+            Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
+            Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
         ])
 
         cls.form_data = {
             'name': 'Cluster X',
-            'group': clustergroup.pk,
-            'type': clustertype.pk,
+            'group': clustergroups[1].pk,
+            'type': clustertypes[1].pk,
             'tenant': None,
-            'site': site.pk,
+            'site': sites[1].pk,
             'comments': 'Some comments',
             'tags': 'Alpha,Bravo,Charlie',
         }
@@ -95,6 +111,14 @@ class ClusterTestCase(StandardTestCases.Views):
             "Cluster 6,Cluster Type 1",
         )
 
+        cls.bulk_edit_data = {
+            'group': clustergroups[1].pk,
+            'type': clustertypes[1].pk,
+            'tenant': None,
+            'site': sites[1].pk,
+            'comments': 'New comments',
+        }
+
 
 class VirtualMachineTestCase(StandardTestCases.Views):
     model = VirtualMachine
@@ -102,24 +126,39 @@ class VirtualMachineTestCase(StandardTestCases.Views):
     @classmethod
     def setUpTestData(cls):
 
-        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
-        platform = Platform.objects.create(name='Platform 1', slug='platform-1')
+        deviceroles = (
+            DeviceRole(name='Device Role 1', slug='device-role-1'),
+            DeviceRole(name='Device Role 2', slug='device-role-2'),
+        )
+        DeviceRole.objects.bulk_create(deviceroles)
+
+        platforms = (
+            Platform(name='Platform 1', slug='platform-1'),
+            Platform(name='Platform 2', slug='platform-2'),
+        )
+        Platform.objects.bulk_create(platforms)
+
         clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
-        cluster = Cluster.objects.create(name='Cluster 1', type=clustertype)
+
+        clusters = (
+            Cluster(name='Cluster 1', type=clustertype),
+            Cluster(name='Cluster 2', type=clustertype),
+        )
+        Cluster.objects.bulk_create(clusters)
 
         VirtualMachine.objects.bulk_create([
-            VirtualMachine(name='Virtual Machine 1', cluster=cluster),
-            VirtualMachine(name='Virtual Machine 2', cluster=cluster),
-            VirtualMachine(name='Virtual Machine 3', cluster=cluster),
+            VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
+            VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
+            VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
         ])
 
         cls.form_data = {
-            'cluster': cluster.pk,
+            'cluster': clusters[1].pk,
             'tenant': None,
-            'platform': None,
+            'platform': platforms[1].pk,
             'name': 'Virtual Machine X',
             'status': VirtualMachineStatusChoices.STATUS_STAGED,
-            'role': devicerole.pk,
+            'role': deviceroles[1].pk,
             'primary_ip4': None,
             'primary_ip6': None,
             'vcpus': 4,
@@ -136,3 +175,15 @@ class VirtualMachineTestCase(StandardTestCases.Views):
             "Virtual Machine 5,Cluster 1",
             "Virtual Machine 6,Cluster 1",
         )
+
+        cls.bulk_edit_data = {
+            'cluster': clusters[1].pk,
+            'tenant': None,
+            'platform': platforms[1].pk,
+            'status': VirtualMachineStatusChoices.STATUS_STAGED,
+            'role': deviceroles[1].pk,
+            'vcpus': 8,
+            'memory': 65535,
+            'disk': 8000,
+            'comments': 'New comments',
+        }