Closes #1846: Enable MPTT for InventoryItem hierarchy

This commit is contained in:
Jeremy Stretch 2020-09-18 10:18:03 -04:00
parent 0030fe1779
commit 230e7bbe34
12 changed files with 98 additions and 32 deletions

View File

@ -10,6 +10,7 @@
### Other Changes ### 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 * [#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 * [#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 * [#4878](https://github.com/netbox-community/netbox/issues/4878) - Custom field data is now stored directly on each object

View File

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

View File

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

View File

@ -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'),
),
]

View File

@ -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
),
]

View File

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

View File

@ -1273,7 +1273,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
class InventoryItemTest(APIViewTestCases.APIViewTestCase): class InventoryItemTest(APIViewTestCases.APIViewTestCase):
model = InventoryItem model = InventoryItem
brief_fields = ['device', 'id', 'name', 'url'] brief_fields = ['_depth', 'device', 'id', 'name', 'url']
@classmethod @classmethod
def setUpTestData(cls): 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') 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) device = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Device 1', site=site)
inventory_items = ( InventoryItem.objects.create(device=device, name='Inventory Item 1', manufacturer=manufacturer)
InventoryItem(device=device, name='Inventory Item 1', manufacturer=manufacturer), InventoryItem.objects.create(device=device, name='Inventory Item 2', manufacturer=manufacturer)
InventoryItem(device=device, name='Inventory Item 2', manufacturer=manufacturer), InventoryItem.objects.create(device=device, name='Inventory Item 3', manufacturer=manufacturer)
InventoryItem(device=device, name='Inventory Item 3', manufacturer=manufacturer),
)
InventoryItem.objects.bulk_create(inventory_items)
cls.create_data = [ cls.create_data = [
{ {

View File

@ -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[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(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 = ( child_inventory_items = (
InventoryItem(device=devices[0], name='Inventory Item 1A', parent=inventory_items[0]), 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[1], name='Inventory Item 2A', parent=inventory_items[1]),
InventoryItem(device=devices[2], name='Inventory Item 3A', parent=inventory_items[2]), 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): def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]} params = {'id': self.queryset.values_list('pk', flat=True)[:2]}

View File

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

View File

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

View File

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

View File

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