From 6372d065be2cacdc565e0321e7dad981e7879ca3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Aug 2024 16:08:05 -0400 Subject: [PATCH] Enable MPTT for module bays --- netbox/dcim/graphql/types.py | 8 +++- netbox/dcim/migrations/0191_modulebay_mptt.py | 44 +++++++++++++++++++ netbox/dcim/models/device_components.py | 23 +++++++++- netbox/dcim/models/devices.py | 27 +++++++----- netbox/dcim/tables/devices.py | 28 ++++++++---- 5 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 netbox/dcim/migrations/0191_modulebay_mptt.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index bc667240f..24ba5cca4 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -496,12 +496,18 @@ class ModuleType(NetBoxObjectType): @strawberry_django.type( models.ModuleBay, - fields='__all__', + # fields='__all__', + exclude=('parent',), filters=ModuleBayFilter ) class ModuleBayType(ModularComponentType): installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None + children: List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]] + + @strawberry_django.field + def parent(self) -> Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent @strawberry_django.type( diff --git a/netbox/dcim/migrations/0191_modulebay_mptt.py b/netbox/dcim/migrations/0191_modulebay_mptt.py new file mode 100644 index 000000000..87e7c1e8e --- /dev/null +++ b/netbox/dcim/migrations/0191_modulebay_mptt.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.7 on 2024-08-02 20:07 + +import django.db.models.deletion +import mptt.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0190_nested_modules'), + ] + + operations = [ + migrations.AddField( + model_name='modulebay', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='modulebay', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='modulebay', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.modulebay'), + ), + migrations.AddField( + model_name='modulebay', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='modulebay', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6e1a2278d..62312cbf4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Sum +from django.db.models import F, Sum from django.urls import reverse from django.utils.translation import gettext_lazy as _ from mptt.models import MPTTModel, TreeForeignKey @@ -1087,10 +1087,19 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): # Bays # -class ModuleBay(ModularComponentModel, TrackingModelMixin): +class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): """ An empty space within a Device which can house a child device """ + parent = TreeForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='children', + blank=True, + null=True, + editable=False, + db_index=True + ) position = models.CharField( verbose_name=_('position'), max_length=30, @@ -1098,6 +1107,8 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin): help_text=_('Identifier to reference when renaming installed components') ) + objects = TreeManager() + clone_fields = ('device',) class Meta(ModularComponentModel.Meta): @@ -1110,6 +1121,9 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin): verbose_name = _('module bay') verbose_name_plural = _('module bays') + class MPTTMeta: + order_insertion_by = ('module',) + def get_absolute_url(self): return reverse('dcim:modulebay', kwargs={'pk': self.pk}) @@ -1127,6 +1141,11 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin): module_bays.append(module.module_bay.pk) module = module.module_bay.module if module.module_bay else None + def save(self, *args, **kwargs): + if self.module: + self.parent = self.module.module_bay + super().save(*args, **kwargs) + class DeviceBay(ComponentModel, TrackingModelMixin): """ diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 59c846b18..7b470bb2b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1046,7 +1046,8 @@ class Device( self._instantiate_components(self.device_type.interfacetemplates.all()) self._instantiate_components(self.device_type.rearporttemplates.all()) self._instantiate_components(self.device_type.frontporttemplates.all()) - self._instantiate_components(self.device_type.modulebaytemplates.all()) + # Disable bulk_create to accommodate MPTT + self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False) self._instantiate_components(self.device_type.devicebaytemplates.all()) # Disable bulk_create to accommodate MPTT self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False) @@ -1269,17 +1270,19 @@ class Module(PrimaryModel, ConfigContextModel): if not disable_replication: create_instances.append(template_instance) - component_model.objects.bulk_create(create_instances) - # Emit the post_save signal for each newly created object - for component in create_instances: - post_save.send( - sender=component_model, - instance=component, - created=True, - raw=False, - using='default', - update_fields=None - ) + # component_model.objects.bulk_create(create_instances) + # # Emit the post_save signal for each newly created object + # for component in create_instances: + # post_save.send( + # sender=component_model, + # instance=component, + # created=True, + # raw=False, + # using='default', + # update_fields=None + # ) + for instance in create_instances: + instance.save() update_fields = ['module'] component_model.objects.bulk_update(update_instances, update_fields) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index d00e25d8f..6c292cc79 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -313,6 +313,10 @@ class ModularDeviceComponentTable(DeviceComponentTable): verbose_name=_('Inventory Items'), ) + class Meta(NetBoxTable.Meta): + pass + # order_by = ('device', 'module', 'name') + class CableTerminationTable(NetBoxTable): cable = tables.Column( @@ -852,10 +856,9 @@ class ModuleBayTable(ModularDeviceComponentTable): 'args': [Accessor('device_id')], } ) - parent_bay = tables.Column( - accessor=tables.A('module__module_bay'), + parent = tables.Column( linkify=True, - verbose_name=_('Parent Bay') + verbose_name=_('Parent'), ) installed_module = tables.Column( linkify=True, @@ -878,14 +881,14 @@ class ModuleBayTable(ModularDeviceComponentTable): verbose_name=_('Module Status') ) - class Meta(DeviceComponentTable.Meta): + class Meta(ModularDeviceComponentTable.Meta): model = models.ModuleBay fields = ( - 'pk', 'id', 'name', 'device', 'parent_bay', 'label', 'position', 'installed_module', 'module_status', + 'pk', 'id', 'name', 'device', 'parent', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag', 'description', 'tags', ) default_columns = ( - 'pk', 'name', 'device', 'parent_bay', 'label', 'installed_module', 'module_status', 'description', + 'pk', 'name', 'device', 'parent', 'label', 'installed_module', 'module_status', 'description', ) def render_parent_bay(self, value): @@ -896,17 +899,24 @@ class ModuleBayTable(ModularDeviceComponentTable): class DeviceModuleBayTable(ModuleBayTable): + name = tables.TemplateColumn( + verbose_name=_('Name'), + template_code='' + '{{ value }}', + order_by=Accessor('_name'), + attrs={'td': {'class': 'text-nowrap'}} + ) actions = columns.ActionsColumn( extra_buttons=MODULEBAY_BUTTONS ) - class Meta(DeviceComponentTable.Meta): + class Meta(ModuleBayTable.Meta): model = models.ModuleBay fields = ( - 'pk', 'id', 'parent_bay', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', + 'pk', 'id', 'parent', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag', 'description', 'tags', 'actions', ) - default_columns = ('pk', 'parent_bay', 'name', 'label', 'installed_module', 'module_status', 'description') + default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description') class InventoryItemTable(DeviceComponentTable):