From 230e7bbe34c441bd7a687e048d4bbca45f4ee670 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 18 Sep 2020 10:18:03 -0400 Subject: [PATCH] Closes #1846: Enable MPTT for InventoryItem hierarchy --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/api/nested_serializers.py | 3 +- netbox/dcim/api/serializers.py | 3 +- .../migrations/0117_inventoryitem_mptt.py | 44 +++++++++++++++++++ .../0118_inventoryitem_mptt_rebuild.py | 26 +++++++++++ netbox/dcim/models/device_components.py | 11 +++-- netbox/dcim/tests/test_api.py | 11 ++--- netbox/dcim/tests/test_filters.py | 6 ++- netbox/dcim/tests/test_views.py | 8 ++-- netbox/dcim/views.py | 6 +-- netbox/templates/dcim/device_inventory.html | 4 +- netbox/templates/dcim/inc/inventoryitem.html | 7 +-- 12 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 netbox/dcim/migrations/0117_inventoryitem_mptt.py create mode 100644 netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index e9dfbc0c39..48f5ae1d74 100644 --- a/docs/release-notes/version-2.10.md +++ b/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 diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 3bc9539918..40b03ada6f 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -305,10 +305,11 @@ class Meta: 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'] # diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1b2f752014..7256393212 100644 --- a/netbox/dcim/api/serializers.py +++ b/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', ] diff --git a/netbox/dcim/migrations/0117_inventoryitem_mptt.py b/netbox/dcim/migrations/0117_inventoryitem_mptt.py new file mode 100644 index 0000000000..2e7b34a8d0 --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py b/netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py new file mode 100644 index 0000000000..4bd0c770fe --- /dev/null +++ b/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 + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4d79f34346..57c611bc96 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -6,6 +6,7 @@ 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 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 @@ def clean(self): # @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', ] diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 22085fdbdc..286405e546 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1273,7 +1273,7 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): 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 = [ { diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index d4504d5862..0a2794f01f 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -2285,14 +2285,16 @@ def setUpTestData(cls): 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]} diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 066ea1b029..7afde8ed25 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1430,11 +1430,9 @@ def setUpTestData(cls): 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') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f9a04aede9..0e322b2d37 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1089,10 +1089,8 @@ def get(self, request, pk): 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, diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index 69afbb6a18..3668d30528 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -30,9 +30,7 @@ {% for item in inventory_items %} - {% with template_name='dcim/inc/inventoryitem.html' indent=0 %} - {% include template_name %} - {% endwith %} + {% include 'dcim/inc/inventoryitem.html' %} {% endfor %} diff --git a/netbox/templates/dcim/inc/inventoryitem.html b/netbox/templates/dcim/inc/inventoryitem.html index 1b103893f2..9bcfffa727 100644 --- a/netbox/templates/dcim/inc/inventoryitem.html +++ b/netbox/templates/dcim/inc/inventoryitem.html @@ -8,7 +8,7 @@ {% endif %} - + {{ item }} @@ -38,8 +38,3 @@ {% endif %} -{% for item in item.child_items.all %} - {% with template_name='dcim/inc/inventoryitem.html' indent=indent|add:20 %} - {% include template_name %} - {% endwith %} -{% endfor %}