mirror of
https://github.com/netbox-community/netbox.git
synced 2025-09-06 06:13:36 -06:00
Merge pull request #20047 from netbox-community/19740-platform-nesting
Closes #19740: Enable recursive nesting for platforms
This commit is contained in:
commit
032bd52dc7
@ -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.
|
||||
|
@ -6,11 +6,13 @@ from dcim import models
|
||||
|
||||
__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']
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -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 DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
|
||||
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(
|
||||
|
@ -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):
|
||||
|
@ -504,6 +504,16 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -536,6 +536,11 @@ class DeviceRoleForm(NetBoxModelForm):
|
||||
|
||||
|
||||
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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
55
netbox/dcim/migrations/0211_platform_parent.py
Normal file
55
netbox/dcim/migrations/0211_platform_parent.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
29
netbox/dcim/migrations/0212_platform_rebuild.py
Normal file
29
netbox/dcim/migrations/0212_platform_rebuild.py
Normal file
@ -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
|
||||
),
|
||||
]
|
@ -424,7 +424,7 @@ class DeviceRole(NestedGroupModel):
|
||||
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')
|
||||
|
@ -103,7 +103,7 @@ class DeviceRoleTable(NetBoxTable):
|
||||
#
|
||||
|
||||
class PlatformTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
name = columns.MPTTColumn(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
|
@ -1247,7 +1247,9 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
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 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
||||
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):
|
||||
|
@ -1256,7 +1256,8 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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)
|
||||
|
@ -619,7 +619,8 @@ class DeviceTypeTestCase(
|
||||
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 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
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 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
|
||||
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 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
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(
|
||||
|
@ -931,7 +931,8 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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'),
|
||||
|
@ -33,6 +33,10 @@
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify|placeholder }}</td>
|
||||
@ -49,11 +53,25 @@
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Platforms" %}
|
||||
{% if perms.dcim.add_platform %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:platform_add' %}?parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Platform" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:platform_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -415,7 +415,8 @@ class DynamicFilterLookupExpressionTest(TestCase):
|
||||
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'),
|
||||
|
@ -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)'),
|
||||
)
|
||||
|
@ -287,7 +287,8 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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'),
|
||||
|
@ -210,7 +210,8 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
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'),
|
||||
|
Loading…
Reference in New Issue
Block a user