2
0
Эх сурвалжийг харах

Closes #1846: Enable MPTT for InventoryItem hierarchy

Jeremy Stretch 5 жил өмнө
parent
commit
230e7bbe34

+ 1 - 0
docs/release-notes/version-2.10.md

@@ -10,6 +10,7 @@
 
 ### Other Changes
 
+* [#1846](https://github.com/netbox-community/netbox/issues/1846) - Enable MPTT for InventoryItem hierarchy
 * [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs
 * [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates
 * [#4878](https://github.com/netbox-community/netbox/issues/4878) - Custom field data is now stored directly on each object

+ 2 - 1
netbox/dcim/api/nested_serializers.py

@@ -305,10 +305,11 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
 class NestedInventoryItemSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
     device = NestedDeviceSerializer(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
     class Meta:
         model = models.InventoryItem
-        fields = ['id', 'url', 'device', 'name']
+        fields = ['id', 'url', 'device', 'name', '_depth']
 
 
 #

+ 2 - 1
netbox/dcim/api/serializers.py

@@ -636,12 +636,13 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
     # Provide a default value to satisfy UniqueTogetherValidator
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
     class Meta:
         model = InventoryItem
         fields = [
             'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag',
-            'discovered', 'description', 'tags',
+            'discovered', 'description', 'tags', '_depth',
         ]
 
 

+ 44 - 0
netbox/dcim/migrations/0117_inventoryitem_mptt.py

@@ -0,0 +1,44 @@
+from django.db import migrations, models
+import django.db.models.deletion
+import mptt.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0116_custom_field_data'),
+    ]
+
+    operations = [
+        # The MPTT will be rebuilt in the following migration. Using dummy values for now.
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='level',
+            field=models.PositiveIntegerField(default=0, editable=False),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='lft',
+            field=models.PositiveIntegerField(default=0, editable=False),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='rght',
+            field=models.PositiveIntegerField(default=0, editable=False),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='tree_id',
+            field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
+            preserve_default=False,
+        ),
+        # Convert ForeignKey to TreeForeignKey
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='parent',
+            field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.inventoryitem'),
+        ),
+    ]

+ 26 - 0
netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py

@@ -0,0 +1,26 @@
+from django.db import migrations
+import mptt
+import mptt.managers
+
+
+def rebuild_mptt(apps, schema_editor):
+    manager = mptt.managers.TreeManager()
+    InventoryItem = apps.get_model('dcim', 'InventoryItem')
+    manager.model = InventoryItem
+    mptt.register(InventoryItem)
+    manager.contribute_to_class(InventoryItem, 'objects')
+    manager.rebuild()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0117_inventoryitem_mptt'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=rebuild_mptt,
+            reverse_code=migrations.RunPython.noop
+        ),
+    ]

+ 8 - 3
netbox/dcim/models/device_components.py

@@ -6,6 +6,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Sum
 from django.urls import reverse
+from mptt.models import MPTTModel, TreeForeignKey
 from taggit.managers import TaggableManager
 
 from dcim.choices import *
@@ -15,6 +16,7 @@ from dcim.fields import MACAddressField
 from extras.models import ObjectChange, TaggedItem
 from extras.utils import extras_features
 from utilities.fields import NaturalOrderingField
+from utilities.mptt import TreeManager
 from utilities.ordering import naturalize_interface
 from utilities.querysets import RestrictedQuerySet
 from utilities.query_functions import CollateAsChar
@@ -952,17 +954,18 @@ class DeviceBay(ComponentModel):
 #
 
 @extras_features('export_templates', 'webhooks')
-class InventoryItem(ComponentModel):
+class InventoryItem(MPTTModel, ComponentModel):
     """
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     InventoryItems are used only for inventory purposes.
     """
-    parent = models.ForeignKey(
+    parent = TreeForeignKey(
         to='self',
         on_delete=models.CASCADE,
         related_name='child_items',
         blank=True,
-        null=True
+        null=True,
+        db_index=True
     )
     manufacturer = models.ForeignKey(
         to='dcim.Manufacturer',
@@ -997,6 +1000,8 @@ class InventoryItem(ComponentModel):
 
     tags = TaggableManager(through=TaggedItem)
 
+    objects = TreeManager()
+
     csv_headers = [
         'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
     ]

+ 4 - 7
netbox/dcim/tests/test_api.py

@@ -1273,7 +1273,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
 
 class InventoryItemTest(APIViewTestCases.APIViewTestCase):
     model = InventoryItem
-    brief_fields = ['device', 'id', 'name', 'url']
+    brief_fields = ['_depth', 'device', 'id', 'name', 'url']
 
     @classmethod
     def setUpTestData(cls):
@@ -1283,12 +1283,9 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
         devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
         device = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Device 1', site=site)
 
-        inventory_items = (
-            InventoryItem(device=device, name='Inventory Item 1', manufacturer=manufacturer),
-            InventoryItem(device=device, name='Inventory Item 2', manufacturer=manufacturer),
-            InventoryItem(device=device, name='Inventory Item 3', manufacturer=manufacturer),
-        )
-        InventoryItem.objects.bulk_create(inventory_items)
+        InventoryItem.objects.create(device=device, name='Inventory Item 1', manufacturer=manufacturer)
+        InventoryItem.objects.create(device=device, name='Inventory Item 2', manufacturer=manufacturer)
+        InventoryItem.objects.create(device=device, name='Inventory Item 3', manufacturer=manufacturer)
 
         cls.create_data = [
             {

+ 4 - 2
netbox/dcim/tests/test_filters.py

@@ -2285,14 +2285,16 @@ class InventoryItemTestCase(TestCase):
             InventoryItem(device=devices[1], manufacturer=manufacturers[1], name='Inventory Item 2', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
             InventoryItem(device=devices[2], manufacturer=manufacturers[2], name='Inventory Item 3', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
         )
-        InventoryItem.objects.bulk_create(inventory_items)
+        for i in inventory_items:
+            i.save()
 
         child_inventory_items = (
             InventoryItem(device=devices[0], name='Inventory Item 1A', parent=inventory_items[0]),
             InventoryItem(device=devices[1], name='Inventory Item 2A', parent=inventory_items[1]),
             InventoryItem(device=devices[2], name='Inventory Item 3A', parent=inventory_items[2]),
         )
-        InventoryItem.objects.bulk_create(child_inventory_items)
+        for i in child_inventory_items:
+            i.save()
 
     def test_id(self):
         params = {'id': self.queryset.values_list('pk', flat=True)[:2]}

+ 3 - 5
netbox/dcim/tests/test_views.py

@@ -1430,11 +1430,9 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
         device = create_test_device('Device 1')
         manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
 
-        InventoryItem.objects.bulk_create([
-            InventoryItem(device=device, name='Inventory Item 1'),
-            InventoryItem(device=device, name='Inventory Item 2'),
-            InventoryItem(device=device, name='Inventory Item 3'),
-        ])
+        InventoryItem.objects.create(device=device, name='Inventory Item 1')
+        InventoryItem.objects.create(device=device, name='Inventory Item 2')
+        InventoryItem.objects.create(device=device, name='Inventory Item 3')
 
         tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
 

+ 2 - 4
netbox/dcim/views.py

@@ -1089,10 +1089,8 @@ class DeviceInventoryView(ObjectView):
 
         device = get_object_or_404(self.queryset, pk=pk)
         inventory_items = InventoryItem.objects.restrict(request.user, 'view').filter(
-            device=device, parent=None
-        ).prefetch_related(
-            'manufacturer', 'child_items'
-        )
+            device=device
+        ).prefetch_related('manufacturer')
 
         return render(request, 'dcim/device_inventory.html', {
             'device': device,

+ 1 - 3
netbox/templates/dcim/device_inventory.html

@@ -30,9 +30,7 @@
                         </thead>
                         <tbody>
                             {% for item in inventory_items %}
-                                {% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
-                                    {% include template_name %}
-                                {% endwith %}
+                                {% include 'dcim/inc/inventoryitem.html' %}
                             {% endfor %}
                         </tbody>
                     </table>

+ 1 - 6
netbox/templates/dcim/inc/inventoryitem.html

@@ -8,7 +8,7 @@
         </td>
     {% endif %}
 
-    <td style="padding-left: {{ indent|add:5 }}px">
+    <td style="padding-left: {{ item.level }}0px">
         <a href="{{ item.get_absolute_url }}">{{ item }}</a>
     </td>
     <td>
@@ -38,8 +38,3 @@
         {% endif %}
     </td>
 </tr>
-{% for item in item.child_items.all %}
-    {% with template_name='dcim/inc/inventoryitem.html' indent=indent|add:20 %}
-        {% include template_name %}
-    {% endwith %}
-{% endfor %}