mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Closes #1846: Enable MPTT for InventoryItem hierarchy
This commit is contained in:
parent
0030fe1779
commit
230e7bbe34
@ -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
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
44
netbox/dcim/migrations/0117_inventoryitem_mptt.py
Normal file
44
netbox/dcim/migrations/0117_inventoryitem_mptt.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
26
netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py
Normal file
26
netbox/dcim/migrations/0118_inventoryitem_mptt_rebuild.py
Normal 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
|
||||||
|
),
|
||||||
|
]
|
@ -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',
|
||||||
]
|
]
|
||||||
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
@ -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]}
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user