Initial work on #7846

This commit is contained in:
jeremystretch 2021-12-27 13:26:17 -05:00
parent a58f1c6a7d
commit 99d5013de3
11 changed files with 161 additions and 22 deletions

View File

@ -810,17 +810,32 @@ class InventoryItemSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer()
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
role = NestedInventoryItemRoleSerializer(required=False, allow_null=True)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
component_type = ContentTypeField(
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItem
fields = [
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
'custom_fields', 'created', 'last_updated', '_depth',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_component(self, obj):
if obj.component is None:
return None
serializer = get_serializer_for_model(obj.component, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.component, context=context).data
#
# Device component roles

View File

@ -50,16 +50,31 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
#
# PowerFeeds
# Power feeds
#
POWERFEED_VOLTAGE_DEFAULT = 120
POWERFEED_AMPERAGE_DEFAULT = 20
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
#
# Device components
#
MODULAR_COMPONENT_MODELS = Q(
app_label='dcim',
model__in=(
'consoleport',
'consoleserverport',
'frontport',
'interface',
'poweroutlet',
'powerport',
'rearport',
))
#
# Cabling and connections
#

View File

@ -1294,6 +1294,8 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
to_field_name='slug',
label='Role (slug)',
)
component_type = ContentTypeFilter()
component_id = MultiValueNumberFilter()
serial = django_filters.CharFilter(
lookup_expr='iexact'
)

View File

@ -12,8 +12,8 @@ from extras.models import Tag
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
from tenancy.forms import TenancyForm
from utilities.forms import (
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
SlugField, StaticSelect,
)
from virtualization.models import Cluster, ClusterGroup
@ -1376,6 +1376,15 @@ class InventoryItemForm(CustomFieldModelForm):
queryset=Manufacturer.objects.all(),
required=False
)
component_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False,
widget=StaticSelect
)
component_id = forms.IntegerField(
required=False
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
@ -1385,8 +1394,13 @@ class InventoryItemForm(CustomFieldModelForm):
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
'description', 'component_type', 'component_id', 'tags',
]
fieldsets = (
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
('Component', ('component_type', 'component_id')),
)
#

View File

@ -1,4 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from dcim.choices import *
from dcim.constants import *
@ -7,8 +8,8 @@ from extras.forms import CustomFieldModelForm, CustomFieldsMixin
from extras.models import Tag
from ipam.models import VLAN
from utilities.forms import (
add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ExpandableNameField, StaticSelect,
add_blank_choice, BootstrapMixin, ColorField, ContentTypeChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect,
)
from wireless.choices import *
from .common import InterfaceCommonForm
@ -680,7 +681,16 @@ class InventoryItemCreateForm(ComponentCreateForm):
max_length=50,
required=False,
)
component_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False,
widget=StaticSelect
)
component_id = forms.IntegerField(
required=False
)
field_order = (
'device', 'parent', 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
'description', 'component_type', 'component_id', 'tags',
)

View File

@ -0,0 +1,23 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0146_inventoryitemrole'),
]
operations = [
migrations.AddField(
model_name='inventoryitem',
name='component_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='inventoryitem',
name='component_type',
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
),
]

View File

@ -97,6 +97,12 @@ class ModularComponentModel(ComponentModel):
blank=True,
null=True
)
inventory_items = GenericRelation(
to='dcim.InventoryItem',
content_type_field='component_type',
object_id_field='component_id',
related_name='%(class)ss',
)
class Meta:
abstract = True
@ -994,6 +1000,22 @@ class InventoryItem(MPTTModel, ComponentModel):
null=True,
db_index=True
)
component_type = models.ForeignKey(
to=ContentType,
limit_choices_to=MODULAR_COMPONENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
blank=True,
null=True
)
component_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
component = GenericForeignKey(
ct_field='component_type',
fk_field='component_id'
)
role = models.ForeignKey(
to='dcim.InventoryItemRole',
on_delete=models.PROTECT,

View File

@ -780,6 +780,9 @@ class InventoryItemTable(DeviceComponentTable):
manufacturer = tables.Column(
linkify=True
)
component = tables.Column(
linkify=True
)
discovered = BooleanColumn()
tags = TagColumn(
url_name='dcim:inventoryitem_list'
@ -790,9 +793,11 @@ class InventoryItemTable(DeviceComponentTable):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'discovered', 'tags',
'component', 'description', 'discovered', 'tags',
)
default_columns = (
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
)
default_columns = ('pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag')
class DeviceInventoryItemTable(InventoryItemTable):
@ -810,11 +815,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
class Meta(BaseTable.Meta):
model = InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'discovered', 'tags', 'actions',
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'actions',
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions',
)

View File

@ -1632,9 +1632,16 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
)
InventoryItemRole.objects.bulk_create(roles)
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer)
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer)
interfaces = (
Interface(device=device, name='Interface 1'),
Interface(device=device, name='Interface 2'),
Interface(device=device, name='Interface 3'),
)
Interface.objects.bulk_create(interfaces)
InventoryItem.objects.create(device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer, component=interfaces[0])
InventoryItem.objects.create(device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer, component=interfaces[1])
InventoryItem.objects.create(device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer, component=interfaces[2])
cls.create_data = [
{
@ -1642,18 +1649,24 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
'name': 'Inventory Item 4',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'component_type': 'dcim.interface',
'component_id': interfaces[0].pk,
},
{
'device': device.pk,
'name': 'Inventory Item 5',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'component_type': 'dcim.interface',
'component_id': interfaces[1].pk,
},
{
'device': device.pk,
'name': 'Inventory Item 6',
'role': roles[1].pk,
'manufacturer': manufacturer.pk,
'component_type': 'dcim.interface',
'component_id': interfaces[2].pk,
},
]

View File

@ -3004,10 +3004,16 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
)
InventoryItemRole.objects.bulk_create(roles)
components = (
Interface.objects.create(device=devices[0], name='Interface 1'),
ConsolePort.objects.create(device=devices[1], name='Console Port 1'),
ConsoleServerPort.objects.create(device=devices[2], name='Console Server Port 1'),
)
inventory_items = (
InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'),
InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second'),
InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third'),
InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]),
InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]),
InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]),
)
for i in inventory_items:
i.save()
@ -3103,6 +3109,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'serial': 'abc'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_component_type(self):
params = {'component_type': 'dcim.interface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = InventoryItemRole.objects.all()

View File

@ -50,6 +50,16 @@
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Component</th>
<td>
{% if object.component %}
<a href="{{ object.component.get_absolute_url }}">{{ object.component }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>