diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md index 35b0b68ebde..3400294e605 100644 --- a/docs/models/dcim/platform.md +++ b/docs/models/dcim/platform.md @@ -2,12 +2,20 @@ A platform defines the type of software running on a [device](./device.md) or [virtual machine](../virtualization/virtualmachine.md). This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15. +Platforms may be nested under parents to form a hierarchy. For example, platforms named "Debian" and "RHEL" might both be created under a generic "Linux" parent. + Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer. -The assignment of platforms to devices is an optional feature, and may be disregarded if not desired. +The assignment of platforms to devices and virtual machines is optional. ## Fields +## Parent + +!!! "This field was introduced in NetBox v4.4." + +The parent platform class to which this platform belongs (optional). + ### Name A human-friendly name for the platform. Must be unique per manufacturer. diff --git a/netbox/dcim/api/serializers_/nested.py b/netbox/dcim/api/serializers_/nested.py index 0e9eaa52fe8..5b1be4d9845 100644 --- a/netbox/dcim/api/serializers_/nested.py +++ b/netbox/dcim/api/serializers_/nested.py @@ -6,11 +6,13 @@ __all__ = ( 'NestedDeviceBaySerializer', + 'NestedDeviceRoleSerializer', 'NestedDeviceSerializer', 'NestedInterfaceSerializer', 'NestedInterfaceTemplateSerializer', 'NestedLocationSerializer', 'NestedModuleBaySerializer', + 'NestedPlatformSerializer', 'NestedRegionSerializer', 'NestedSiteGroupSerializer', ) @@ -102,3 +104,10 @@ class NestedModuleBaySerializer(WritableNestedSerializer): class Meta: model = models.ModuleBay fields = ['id', 'url', 'display_url', 'display', 'name'] + + +class NestedPlatformSerializer(WritableNestedSerializer): + + class Meta: + model = models.Platform + fields = ['id', 'url', 'display_url', 'display', 'name'] diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py index 2f47457019f..c357b0bbe81 100644 --- a/netbox/dcim/api/serializers_/platforms.py +++ b/netbox/dcim/api/serializers_/platforms.py @@ -1,15 +1,17 @@ from dcim.models import Platform from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer from .manufacturers import ManufacturerSerializer +from .nested import NestedPlatformSerializer __all__ = ( 'PlatformSerializer', ) -class PlatformSerializer(NetBoxModelSerializer): +class PlatformSerializer(NestedGroupModelSerializer): + parent = NestedPlatformSerializer(required=False, allow_null=True, default=None) manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) @@ -20,7 +22,10 @@ class PlatformSerializer(NetBoxModelSerializer): class Meta: model = Platform fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + 'id', 'url', 'display_url', 'display', 'parent', 'name', 'slug', 'manufacturer', 'config_template', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', + 'virtualmachine_count', '_depth', ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + brief_fields = ( + 'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth', + ) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 7f149355734..b75febd7282 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -547,14 +547,17 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet): to_field_name='slug', label=_('Manufacturer (slug)'), ) - default_platform_id = django_filters.ModelMultipleChoiceFilter( + default_platform_id = TreeNodeMultipleChoiceFilter( queryset=Platform.objects.all(), + field_name='default_platform', + lookup_expr='in', label=_('Default platform (ID)'), ) - default_platform = django_filters.ModelMultipleChoiceFilter( - field_name='default_platform__slug', + default_platform = TreeNodeMultipleChoiceFilter( queryset=Platform.objects.all(), + field_name='default_platform', to_field_name='slug', + lookup_expr='in', label=_('Default platform (slug)'), ) has_front_image = django_filters.BooleanFilter( @@ -979,6 +982,29 @@ class Meta: class PlatformFilterSet(OrganizationalModelFilterSet): + parent_id = django_filters.ModelMultipleChoiceFilter( + queryset=Platform.objects.all(), + label=_('Immediate parent platform (ID)'), + ) + parent = django_filters.ModelMultipleChoiceFilter( + field_name='parent__slug', + queryset=Platform.objects.all(), + to_field_name='slug', + label=_('Immediate parent platform (slug)'), + ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=Platform.objects.all(), + field_name='parent', + lookup_expr='in', + label=_('Parent platform (ID)'), + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=Platform.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug', + label=_('Parent platform (slug)'), + ) manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', queryset=Manufacturer.objects.all(), @@ -1058,14 +1084,17 @@ class DeviceFilterSet( queryset=Device.objects.all(), label=_('Parent Device (ID)'), ) - platform_id = django_filters.ModelMultipleChoiceFilter( + platform_id = TreeNodeMultipleChoiceFilter( queryset=Platform.objects.all(), + field_name='platform', + lookup_expr='in', label=_('Platform (ID)'), ) - platform = django_filters.ModelMultipleChoiceFilter( - field_name='platform__slug', + platform = TreeNodeMultipleChoiceFilter( + field_name='platform', queryset=Platform.objects.all(), to_field_name='slug', + lookup_expr='in', label=_('Platform (slug)'), ) region_id = TreeNodeMultipleChoiceFilter( diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 5f70683aede..587b7dbde59 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -682,6 +682,11 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): class PlatformBulkEditForm(NetBoxModelBulkEditForm): + parent = DynamicModelChoiceField( + label=_('Parent'), + queryset=Platform.objects.all(), + required=False, + ) manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -697,12 +702,13 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = Platform fieldsets = ( - FieldSet('manufacturer', 'config_template', 'description'), + FieldSet('parent', 'manufacturer', 'config_template', 'description'), ) - nullable_fields = ('manufacturer', 'config_template', 'description') + nullable_fields = ('parent', 'manufacturer', 'config_template', 'description', 'comments') class DeviceBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index fc33c2162e9..be47f1fc05d 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -504,6 +504,16 @@ class Meta: class PlatformImportForm(NetBoxModelImportForm): slug = SlugField() + parent = CSVModelChoiceField( + label=_('Parent'), + queryset=Platform.objects.all(), + required=False, + to_field_name='name', + help_text=_('Parent platform'), + error_messages={ + 'invalid_choice': _('Platform not found.'), + } + ) manufacturer = CSVModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -522,7 +532,7 @@ class PlatformImportForm(NetBoxModelImportForm): class Meta: model = Platform fields = ( - 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 8e75695099f..3c7a5754631 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -714,6 +714,11 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm): class PlatformFilterForm(NetBoxModelFilterSetForm): model = Platform selector_fields = ('filter_id', 'q', 'manufacturer_id') + parent_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + label=_('Parent') + ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 6454e1d141e..bdaa1f0e3a9 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -536,6 +536,11 @@ class Meta: class PlatformForm(NetBoxModelForm): + parent = DynamicModelChoiceField( + label=_('Parent'), + queryset=Platform.objects.all(), + required=False, + ) manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -551,15 +556,18 @@ class PlatformForm(NetBoxModelForm): label=_('Slug'), max_length=64 ) + comments = CommentField() fieldsets = ( - FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')), + FieldSet( + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'), + ), ) class Meta: model = Platform fields = [ - 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'comments', 'tags', ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 8b1755e3583..0cd5e8fd11b 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -633,6 +633,8 @@ class ModuleTypeType(NetBoxObjectType): pagination=True ) class PlatformType(OrganizationalObjectType): + parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None + children: List[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]] manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None diff --git a/netbox/dcim/migrations/0211_platform_parent.py b/netbox/dcim/migrations/0211_platform_parent.py new file mode 100644 index 00000000000..e5b5c6bc3b7 --- /dev/null +++ b/netbox/dcim/migrations/0211_platform_parent.py @@ -0,0 +1,55 @@ +import django.db.models.deletion +import mptt.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0210_interface_tx_power_negative'), + ] + + operations = [ + # Add parent & MPTT fields + migrations.AddField( + model_name='platform', + name='parent', + field=mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='children', + to='dcim.platform' + ), + ), + migrations.AddField( + model_name='platform', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='platform', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='platform', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='platform', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + # Add comments field + migrations.AddField( + model_name='platform', + name='comments', + field=models.TextField(blank=True), + ), + ] diff --git a/netbox/dcim/migrations/0212_platform_rebuild.py b/netbox/dcim/migrations/0212_platform_rebuild.py new file mode 100644 index 00000000000..b15ffd281d4 --- /dev/null +++ b/netbox/dcim/migrations/0212_platform_rebuild.py @@ -0,0 +1,29 @@ +from django.db import migrations +import mptt +import mptt.managers + + +def rebuild_mptt(apps, schema_editor): + """ + Construct the MPTT hierarchy. + """ + Platform = apps.get_model('dcim', 'Platform') + manager = mptt.managers.TreeManager() + manager.model = Platform + mptt.register(Platform) + manager.contribute_to_class(Platform, 'objects') + manager.rebuild() + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0211_platform_parent'), + ] + + operations = [ + migrations.RunPython( + code=rebuild_mptt, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 78fd881a702..ab4aeb12813 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -424,7 +424,7 @@ class Meta: verbose_name_plural = _('device roles') -class Platform(OrganizationalModel): +class Platform(NestedGroupModel): """ Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". A Platform may optionally be associated with a particular Manufacturer. @@ -454,6 +454,8 @@ class Platform(OrganizationalModel): null=True ) + clone_fields = ('parent', 'description') + class Meta: ordering = ('name',) verbose_name = _('platform') diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f0465a1b539..d63a098042e 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -103,7 +103,7 @@ class Meta(NetBoxTable.Meta): # class PlatformTable(NetBoxTable): - name = tables.Column( + name = columns.MPTTColumn( verbose_name=_('Name'), linkify=True ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 8af539b047f..cefbc7b521e 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1247,7 +1247,9 @@ def setUpTestData(cls): class PlatformTest(APIViewTestCases.APIViewTestCase): model = Platform - brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count'] + brief_fields = [ + '_depth', 'description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count', + ] create_data = [ { 'name': 'Platform 4', @@ -1274,7 +1276,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2'), Platform(name='Platform 3', slug='platform-3'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() class DeviceTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 2ae178653a2..f0701ee4b4f 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1256,7 +1256,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1]), Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2]), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() device_types = ( DeviceType( @@ -2435,7 +2436,37 @@ def setUpTestData(cls): Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'), Platform(name='Platform 4', slug='platform-4'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() + child_platforms = ( + Platform(parent=platforms[0], name='Platform 1A', slug='platform-1a', manufacturer=manufacturers[0]), + Platform(parent=platforms[1], name='Platform 2A', slug='platform-2a', manufacturer=manufacturers[1]), + Platform(parent=platforms[2], name='Platform 3A', slug='platform-3a', manufacturer=manufacturers[2]), + ) + for platform in child_platforms: + platform.save() + grandchild_platforms = ( + Platform( + parent=child_platforms[0], + name='Platform 1A1', + slug='platform-1a1', + manufacturer=manufacturers[0], + ), + Platform( + parent=child_platforms[1], + name='Platform 2A1', + slug='platform-2a1', + manufacturer=manufacturers[1], + ), + Platform( + parent=child_platforms[2], + name='Platform 3A1', + slug='platform-3a1', + manufacturer=manufacturers[2], + ), + ) + for platform in grandchild_platforms: + platform.save() def test_q(self): params = {'q': 'foobar1'} @@ -2453,12 +2484,26 @@ def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_parent(self): + platforms = Platform.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [platforms[0].pk, platforms[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'parent': [platforms[0].slug, platforms[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_ancestor(self): + platforms = Platform.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [platforms[0].pk, platforms[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'ancestor': [platforms[0].slug, platforms[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_manufacturer(self): manufacturers = Manufacturer.objects.all()[:2] params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_available_for_device_type(self): manufacturers = Manufacturer.objects.all()[:2] @@ -2469,7 +2514,7 @@ def test_available_for_device_type(self): u_height=1 ) params = {'available_for_device_type': device_type.pk} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): @@ -2507,7 +2552,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2'), Platform(name='Platform 3', slug='platform-3'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() regions = ( Region(name='Region 1', slug='region-1'), @@ -2763,7 +2809,7 @@ def test_devicetype(self): params = {'device_type': [device_types[0].slug, device_types[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_devicerole(self): + def test_role(self): roles = DeviceRole.objects.all()[:2] params = {'role_id': [roles[0].pk, roles[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 5e41b37f757..42a30e4f9dd 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -619,7 +619,8 @@ def setUpTestData(cls): Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0]), Platform(name='Platform 2', slug='platform-3', manufacturer=manufacturers[1]), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() DeviceType.objects.bulk_create([ DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]), @@ -1891,7 +1892,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer), Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() tags = create_tags('Alpha', 'Bravo', 'Charlie') @@ -1912,9 +1914,9 @@ def setUpTestData(cls): cls.csv_update_data = ( "id,name,description", - f"{platforms[0].pk},Platform 7,Fourth platform7", - f"{platforms[1].pk},Platform 8,Fifth platform8", - f"{platforms[2].pk},Platform 9,Sixth platform9", + f"{platforms[0].pk},Foo,New description", + f"{platforms[1].pk},Bar,New description", + f"{platforms[2].pk},Baz,New description", ) cls.bulk_edit_data = { @@ -1962,7 +1964,8 @@ def setUpTestData(cls): Platform(name='Platform 1', slug='platform-1'), Platform(name='Platform 2', slug='platform-2'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() devices = ( Device( diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index f9147a30c9b..5c943b74cd2 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -931,7 +931,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2'), Platform(name='Platform 3', slug='platform-3'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() cluster_types = ( ClusterType(name='Cluster Type 1', slug='cluster-type-1'), diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index bed496625bb..4becc042b20 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -33,6 +33,10 @@

{% trans "Platform" %}

{% trans "Description" %} {{ object.description|placeholder }} + + {% trans "Parent" %} + {{ object.parent|linkify|placeholder }} + {% trans "Manufacturer" %} {{ object.manufacturer|linkify|placeholder }} @@ -49,11 +53,25 @@

{% trans "Platform" %}

{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
+
+

+ {% trans "Child Platforms" %} + {% if perms.dcim.add_platform %} + + {% endif %} +

+ {% htmx_table 'dcim:platform_list' parent_id=object.pk %} +
{% plugin_full_width_page object %}
diff --git a/netbox/utilities/tests/test_filters.py b/netbox/utilities/tests/test_filters.py index 97fde51162e..3c24fe10ccf 100644 --- a/netbox/utilities/tests/test_filters.py +++ b/netbox/utilities/tests/test_filters.py @@ -415,7 +415,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2'), Platform(name='Platform 3', slug='platform-3'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() regions = ( Region(name='Region 1', slug='region-1'), diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 06a38da3689..802e34e005b 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -183,13 +183,16 @@ class VirtualMachineFilterSet( to_field_name='slug', label=_('Role (slug)'), ) - platform_id = django_filters.ModelMultipleChoiceFilter( + platform_id = TreeNodeMultipleChoiceFilter( queryset=Platform.objects.all(), + field_name='platform', + lookup_expr='in', label=_('Platform (ID)'), ) - platform = django_filters.ModelMultipleChoiceFilter( - field_name='platform__slug', + platform = TreeNodeMultipleChoiceFilter( queryset=Platform.objects.all(), + field_name='platform', + lookup_expr='in', to_field_name='slug', label=_('Platform (slug)'), ) diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index 228cf66ed0f..0179069af06 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -287,7 +287,8 @@ def setUpTestData(cls): Platform(name='Platform 2', slug='platform-2'), Platform(name='Platform 3', slug='platform-3'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() roles = ( DeviceRole(name='Device Role 1', slug='device-role-1'), diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 27b1a08a5a4..35226c16dc3 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -210,7 +210,8 @@ def setUpTestData(cls): Platform(name='Platform 1', slug='platform-1'), Platform(name='Platform 2', slug='platform-2'), ) - Platform.objects.bulk_create(platforms) + for platform in platforms: + platform.save() sites = ( Site(name='Site 1', slug='site-1'),