Initial work on #8118

This commit is contained in:
jeremystretch 2021-12-29 15:02:25 -05:00
parent 3bb485d0b8
commit 4c15f4a84f
21 changed files with 712 additions and 32 deletions

View File

@ -21,6 +21,7 @@ __all__ = [
'NestedInterfaceTemplateSerializer', 'NestedInterfaceTemplateSerializer',
'NestedInventoryItemSerializer', 'NestedInventoryItemSerializer',
'NestedInventoryItemRoleSerializer', 'NestedInventoryItemRoleSerializer',
'NestedInventoryItemTemplateSerializer',
'NestedManufacturerSerializer', 'NestedManufacturerSerializer',
'NestedModuleBaySerializer', 'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer', 'NestedModuleBayTemplateSerializer',
@ -231,6 +232,15 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = models.InventoryItemTemplate
fields = ['id', 'url', 'display', 'name', '_depth']
# #
# Devices # Devices
# #

View File

@ -447,6 +447,40 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
device_type = NestedDeviceTypeSerializer()
parent = serializers.PrimaryKeyRelatedField(
queryset=InventoryItemTemplate.objects.all(),
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_TEMPLATE_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItemTemplate
fields = [
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
'description', 'component_type', 'component_id', 'component', '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
# #
# Devices # Devices
# #

View File

@ -31,6 +31,7 @@ router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register('rear-port-templates', views.RearPortTemplateViewSet) router.register('rear-port-templates', views.RearPortTemplateViewSet)
router.register('module-bay-templates', views.ModuleBayTemplateViewSet) router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
router.register('device-bay-templates', views.DeviceBayTemplateViewSet) router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
router.register('inventory-item-templates', views.InventoryItemTemplateViewSet)
# Device/modules # Device/modules
router.register('device-roles', views.DeviceRoleViewSet) router.register('device-roles', views.DeviceRoleViewSet)

View File

@ -350,6 +350,12 @@ class DeviceBayTemplateViewSet(ModelViewSet):
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(ModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
# #
# Device roles # Device roles
# #

View File

@ -62,6 +62,18 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# Device components # Device components
# #
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
app_label='dcim',
model__in=(
'consoleporttemplate',
'consoleserverporttemplate',
'frontporttemplate',
'interfacetemplate',
'poweroutlettemplate',
'powerporttemplate',
'rearporttemplate',
))
MODULAR_COMPONENT_MODELS = Q( MODULAR_COMPONENT_MODELS = Q(
app_label='dcim', app_label='dcim',
model__in=( model__in=(

View File

@ -40,6 +40,7 @@ __all__ = (
'InterfaceTemplateFilterSet', 'InterfaceTemplateFilterSet',
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'InventoryItemRoleFilterSet', 'InventoryItemRoleFilterSet',
'InventoryItemTemplateFilterSet',
'LocationFilterSet', 'LocationFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'ModuleBayFilterSet', 'ModuleBayFilterSet',
@ -687,6 +688,49 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name'] fields = ['id', 'name']
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemTemplate.objects.all(),
label='Parent inventory item (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItemRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
field_name='role__slug',
queryset=InventoryItemRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
component_type = ContentTypeFilter()
component_id = MultiValueNumberFilter()
class Meta:
model = InventoryItemTemplate
fields = ['id', 'name', 'label', 'part_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(part_id__icontains=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)
class DeviceRoleFilterSet(OrganizationalModelFilterSet): class DeviceRoleFilterSet(OrganizationalModelFilterSet):
tag = TagFilter() tag = TagFilter()

View File

@ -31,6 +31,7 @@ __all__ = (
'InterfaceTemplateBulkEditForm', 'InterfaceTemplateBulkEditForm',
'InventoryItemBulkEditForm', 'InventoryItemBulkEditForm',
'InventoryItemRoleBulkEditForm', 'InventoryItemRoleBulkEditForm',
'InventoryItemTemplateBulkEditForm',
'LocationBulkEditForm', 'LocationBulkEditForm',
'ManufacturerBulkEditForm', 'ManufacturerBulkEditForm',
'ModuleBulkEditForm', 'ModuleBulkEditForm',
@ -907,6 +908,31 @@ class DeviceBayTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'description') nullable_fields = ('label', 'description')
class InventoryItemTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=InventoryItemTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
max_length=64,
required=False
)
description = forms.CharField(
required=False
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
class Meta:
nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description']
# #
# Device components # Device components
# #

View File

@ -38,6 +38,7 @@ __all__ = (
'InterfaceTemplateForm', 'InterfaceTemplateForm',
'InventoryItemForm', 'InventoryItemForm',
'InventoryItemRoleForm', 'InventoryItemRoleForm',
'InventoryItemTemplateForm',
'LocationForm', 'LocationForm',
'ManufacturerForm', 'ManufacturerForm',
'ModuleForm', 'ModuleForm',
@ -1073,6 +1074,48 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
query_params={
'device_id': '$device'
}
)
role = DynamicModelChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
component_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False,
widget=forms.HiddenInput
)
component_id = forms.IntegerField(
required=False,
widget=forms.HiddenInput
)
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id',
]
fieldsets = (
('Inventory Item', ('device_type', 'parent', 'name', 'label', 'role', 'description')),
('Hardware', ('manufacturer', 'part_id')),
)
widgets = {
'device_type': forms.HiddenInput(),
}
# #
# Device components # Device components
# #

View File

@ -11,6 +11,7 @@ __all__ = (
'DeviceTypeImportForm', 'DeviceTypeImportForm',
'FrontPortTemplateImportForm', 'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm', 'InterfaceTemplateImportForm',
'InventoryItemTemplateImportForm',
'ModuleBayTemplateImportForm', 'ModuleBayTemplateImportForm',
'ModuleTypeImportForm', 'ModuleTypeImportForm',
'PowerOutletTemplateImportForm', 'PowerOutletTemplateImportForm',
@ -49,24 +50,7 @@ class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
# #
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
pass
def clean_device_type(self):
# Limit fields referencing other components to the parent DeviceType
if data := self.cleaned_data['device_type']:
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name not in ['device_type', 'module_type']:
field.queryset = field.queryset.filter(device_type=data)
return data
def clean_module_type(self):
# Limit fields referencing other components to the parent ModuleType
if data := self.cleaned_data['module_type']:
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name not in ['device_type', 'module_type']:
field.queryset = field.queryset.filter(module_type=data)
return data
class ConsolePortTemplateImportForm(ComponentTemplateImportForm): class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
@ -109,6 +93,20 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
] ]
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
power_port = self.fields['power_port']
power_port.queryset = power_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
power_port = self.fields['power_port']
power_port.queryset = power_port.queryset.filter(module_type=module_type)
return module_type
class InterfaceTemplateImportForm(ComponentTemplateImportForm): class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField( type = forms.ChoiceField(
@ -131,6 +129,20 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
to_field_name='name' to_field_name='name'
) )
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
return module_type
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
@ -166,3 +178,40 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
fields = [ fields = [
'device_type', 'name', 'label', 'description', 'device_type', 'name', 'label', 'description',
] ]
class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
parent = forms.ModelChoiceField(
queryset=InventoryItemTemplate.objects.all(),
required=False
)
role = forms.ModelChoiceField(
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
]
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
parent = self.fields['parent']
parent.queryset = parent.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
parent = self.fields['parent']
parent.queryset = parent.queryset.filter(module_type=module_type)
return module_type

View File

@ -53,6 +53,9 @@ class DCIMQuery(graphene.ObjectType):
inventory_item_role = ObjectField(InventoryItemRoleType) inventory_item_role = ObjectField(InventoryItemRoleType)
inventory_item_role_list = ObjectListField(InventoryItemRoleType) inventory_item_role_list = ObjectListField(InventoryItemRoleType)
inventory_item_template = ObjectField(InventoryItemTemplateType)
inventory_item_template_list = ObjectListField(InventoryItemTemplateType)
location = ObjectField(LocationType) location = ObjectField(LocationType)
location_list = ObjectListField(LocationType) location_list = ObjectListField(LocationType)

View File

@ -26,6 +26,7 @@ __all__ = (
'InterfaceTemplateType', 'InterfaceTemplateType',
'InventoryItemType', 'InventoryItemType',
'InventoryItemRoleType', 'InventoryItemRoleType',
'InventoryItemTemplateType',
'LocationType', 'LocationType',
'ManufacturerType', 'ManufacturerType',
'ModuleType', 'ModuleType',
@ -172,6 +173,14 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.InventoryItemTemplate
fields = '__all__'
filterset_class = filtersets.InventoryItemTemplateFilterSet
class DeviceRoleType(OrganizationalObjectType): class DeviceRoleType(OrganizationalObjectType):
class Meta: class Meta:

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
import utilities.fields
import utilities.ordering
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0147_inventoryitem_component'),
]
operations = [
migrations.CreateModel(
name='InventoryItemTemplate',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
('label', models.CharField(blank=True, max_length=64)),
('description', models.CharField(blank=True, max_length=200)),
('component_id', models.PositiveBigIntegerField(blank=True, null=True)),
('part_id', models.CharField(blank=True, max_length=50)),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('component_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventoryitemtemplates', to='dcim.devicetype')),
('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.manufacturer')),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_items', to='dcim.inventoryitemtemplate')),
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_templates', to='dcim.inventoryitemrole')),
],
options={
'ordering': ('device_type__id', 'parent__id', '_name'),
'unique_together': {('device_type', 'parent', 'name')},
},
),
]

View File

@ -1,15 +1,20 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, ModuleBay, PowerOutlet, PowerPort, RearPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort,
) )
@ -19,6 +24,7 @@ __all__ = (
'DeviceBayTemplate', 'DeviceBayTemplate',
'FrontPortTemplate', 'FrontPortTemplate',
'InterfaceTemplate', 'InterfaceTemplate',
'InventoryItemTemplate',
'ModuleBayTemplate', 'ModuleBayTemplate',
'PowerOutletTemplate', 'PowerOutletTemplate',
'PowerPortTemplate', 'PowerPortTemplate',
@ -140,6 +146,8 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
blank=True blank=True
) )
component_model = ConsolePort
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -148,7 +156,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return ConsolePort( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -167,6 +175,8 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
blank=True blank=True
) )
component_model = ConsoleServerPort
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -175,7 +185,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return ConsoleServerPort( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -206,6 +216,8 @@ class PowerPortTemplate(ModularComponentTemplateModel):
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
component_model = PowerPort
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -214,7 +226,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return PowerPort( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -257,6 +269,8 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
help_text="Phase (for three-phase feeds)" help_text="Phase (for three-phase feeds)"
) )
component_model = PowerOutlet
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -283,7 +297,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs) power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
else: else:
power_port = None power_port = None
return PowerOutlet( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -314,6 +328,8 @@ class InterfaceTemplate(ModularComponentTemplateModel):
verbose_name='Management only' verbose_name='Management only'
) )
component_model = Interface
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -322,7 +338,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return Interface( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -356,6 +372,8 @@ class FrontPortTemplate(ModularComponentTemplateModel):
] ]
) )
component_model = FrontPort
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -391,7 +409,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs) rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs)
else: else:
rear_port = None rear_port = None
return FrontPort( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -422,6 +440,8 @@ class RearPortTemplate(ModularComponentTemplateModel):
] ]
) )
component_model = RearPort
class Meta: class Meta:
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
@ -430,7 +450,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return RearPort( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.label, label=self.label,
type=self.type, type=self.type,
@ -451,12 +471,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
help_text='Identifier to reference when renaming installed components' help_text='Identifier to reference when renaming installed components'
) )
component_model = ModuleBay
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', '_name')
unique_together = ('device_type', 'name') unique_together = ('device_type', 'name')
def instantiate(self, device): def instantiate(self, device):
return ModuleBay( return self.component_model(
device=device, device=device,
name=self.name, name=self.name,
label=self.label, label=self.label,
@ -469,12 +491,14 @@ class DeviceBayTemplate(ComponentTemplateModel):
""" """
A template for a DeviceBay to be created for a new parent Device. A template for a DeviceBay to be created for a new parent Device.
""" """
component_model = DeviceBay
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', '_name')
unique_together = ('device_type', 'name') unique_together = ('device_type', 'name')
def instantiate(self, device): def instantiate(self, device):
return DeviceBay( return self.component_model(
device=device, device=device,
name=self.name, name=self.name,
label=self.label label=self.label
@ -485,3 +509,79 @@ class DeviceBayTemplate(ComponentTemplateModel):
raise ValidationError( raise ValidationError(
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays." f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
) )
@extras_features('webhooks')
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
"""
A template for an InventoryItem to be created for a new parent Device.
"""
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='child_items',
blank=True,
null=True,
db_index=True
)
component_type = models.ForeignKey(
to=ContentType,
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_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,
related_name='inventory_item_templates',
blank=True,
null=True
)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
related_name='inventory_item_templates',
blank=True,
null=True
)
part_id = models.CharField(
max_length=50,
verbose_name='Part ID',
blank=True,
help_text='Manufacturer-assigned part identifier'
)
objects = TreeManager()
component_model = InventoryItem
class Meta:
ordering = ('device_type__id', 'parent__id', '_name')
unique_together = ('device_type', 'parent', 'name')
def instantiate(self, **kwargs):
parent = InventoryItemTemplate.objects.get(name=self.parent.name, **kwargs) if self.parent else None
if self.component:
model = self.component.component_model
component = model.objects.get(name=self.component.name, **kwargs)
else:
component = None
return self.component_model(
parent=parent,
name=self.name,
label=self.label,
component=component,
role=self.role,
manufacturer=self.manufacturer,
part_id=self.part_id,
**kwargs
)

View File

@ -933,6 +933,9 @@ class Device(PrimaryModel, ConfigContextModel):
DeviceBay.objects.bulk_create( DeviceBay.objects.bulk_create(
[x.instantiate(device=self) for x in self.device_type.devicebaytemplates.all()] [x.instantiate(device=self) for x in self.device_type.devicebaytemplates.all()]
) )
# Avoid bulk_create to handle MPTT
for x in self.device_type.inventoryitemtemplates.all():
x.instantiate(device=self).save()
# Update Site and Rack assignment for any child Devices # Update Site and Rack assignment for any child Devices
devices = Device.objects.filter(parent_bay__device=self) devices = Device.objects.filter(parent_bay__device=self)

View File

@ -1,8 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import ( from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate, ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
) )
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn, BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
@ -15,6 +16,7 @@ __all__ = (
'DeviceTypeTable', 'DeviceTypeTable',
'FrontPortTemplateTable', 'FrontPortTemplateTable',
'InterfaceTemplateTable', 'InterfaceTemplateTable',
'InventoryItemTemplateTable',
'ManufacturerTable', 'ManufacturerTable',
'ModuleBayTemplateTable', 'ModuleBayTemplateTable',
'PowerOutletTemplateTable', 'PowerOutletTemplateTable',
@ -223,3 +225,25 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions') fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None" empty_text = "None"
class InventoryItemTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=InventoryItemTemplate,
buttons=('edit', 'delete')
)
role = tables.Column(
linkify=True
)
manufacturer = tables.Column(
linkify=True
)
component = tables.Column(
accessor=Accessor('component'),
orderable=False
)
class Meta(ComponentTemplateTable.Meta):
model = InventoryItemTemplate
fields = ('pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions')
empty_text = "None"

View File

@ -897,6 +897,57 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
] ]
class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
model = InventoryItemTemplate
brief_fields = ['_depth', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer,
model='Device Type 1',
slug='device-type-1'
)
role = InventoryItemRole.objects.create(name='Inventory Item Role 1', slug='inventory-item-role-1')
inventory_item_templates = (
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturer, role=role),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturer, role=role),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturer, role=role),
InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 4', manufacturer=manufacturer, role=role),
)
for item in inventory_item_templates:
item.save()
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Inventory Item Template 5',
'manufacturer': manufacturer.pk,
'role': role.pk,
'parent': inventory_item_templates[3].pk,
},
{
'device_type': devicetype.pk,
'name': 'Inventory Item Template 6',
'manufacturer': manufacturer.pk,
'role': role.pk,
'parent': inventory_item_templates[3].pk,
},
{
'device_type': devicetype.pk,
'name': 'Inventory Item Template 7',
'manufacturer': manufacturer.pk,
'role': role.pk,
'parent': inventory_item_templates[3].pk,
},
]
class DeviceRoleTest(APIViewTestCases.APIViewTestCase): class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
model = DeviceRole model = DeviceRole
brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count'] brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']

View File

@ -1214,6 +1214,86 @@ class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class InventoryItemTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = InventoryItemTemplate.objects.all()
filterset = InventoryItemTemplateFilterSet
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
device_types = (
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1'),
DeviceType(manufacturer=manufacturers[0], model='Model 2', slug='model-2'),
DeviceType(manufacturer=manufacturers[0], model='Model 3', slug='model-3'),
)
DeviceType.objects.bulk_create(device_types)
inventory_item_roles = (
InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3'),
)
InventoryItemRole.objects.bulk_create(inventory_item_roles)
inventory_item_templates = (
InventoryItemTemplate(device_type=device_types[0], name='Inventory Item 1', label='A', role=inventory_item_roles[0], manufacturer=manufacturers[0], part_id='1001'),
InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 2', label='B', role=inventory_item_roles[1], manufacturer=manufacturers[1], part_id='1002'),
InventoryItemTemplate(device_type=device_types[2], name='Inventory Item 3', label='C', role=inventory_item_roles[2], manufacturer=manufacturers[2], part_id='1003'),
)
for item in inventory_item_templates:
item.save()
child_inventory_item_templates = (
InventoryItemTemplate(device_type=device_types[0], name='Inventory Item 1A', parent=inventory_item_templates[0]),
InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 2A', parent=inventory_item_templates[1]),
InventoryItemTemplate(device_type=device_types[2], name='Inventory Item 3A', parent=inventory_item_templates[2]),
)
for item in child_inventory_item_templates:
item.save()
def test_name(self):
params = {'name': ['Inventory Item 1', 'Inventory Item 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicetype_id(self):
device_types = DeviceType.objects.all()[:2]
params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_part_id(self):
params = {'part_id': ['1001', '1002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent_id(self):
parent_items = InventoryItemTemplate.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_role(self):
roles = InventoryItemRole.objects.all()[:2]
params = {'role_id': [roles[0].pk, roles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'role': [roles[0].slug, roles[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
filterset = DeviceRoleFilterSet filterset = DeviceRoleFilterSet

View File

@ -580,6 +580,20 @@ class DeviceTypeTestCase(
url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk}) url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200) self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_devicetype_inventoryitems(self):
devicetype = DeviceType.objects.first()
inventory_items = (
DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
)
for inventory_item in inventory_items:
inventory_item.save()
url = reverse('dcim:devicetype_inventoryitems', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_objects(self): def test_import_objects(self):
""" """
@ -659,6 +673,13 @@ device-bays:
- name: Device Bay 1 - name: Device Bay 1
- name: Device Bay 2 - name: Device Bay 2
- name: Device Bay 3 - name: Device Bay 3
inventory-items:
- name: Inventory Item 1
manufacturer: Generic
- name: Inventory Item 2
manufacturer: Generic
- name: Inventory Item 3
manufacturer: Generic
""" """
# Create the manufacturer # Create the manufacturer
@ -677,6 +698,7 @@ device-bays:
'dcim.add_rearporttemplate', 'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate', 'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate', 'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
) )
form_data = { form_data = {
@ -729,13 +751,17 @@ device-bays:
self.assertEqual(fp1.rear_port_position, 1) self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(device_type.modulebaytemplates.count(), 3) self.assertEqual(device_type.modulebaytemplates.count(), 3)
db1 = ModuleBayTemplate.objects.first() mb1 = ModuleBayTemplate.objects.first()
self.assertEqual(db1.name, 'Module Bay 1') self.assertEqual(mb1.name, 'Module Bay 1')
self.assertEqual(device_type.devicebaytemplates.count(), 3) self.assertEqual(device_type.devicebaytemplates.count(), 3)
db1 = DeviceBayTemplate.objects.first() db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1') self.assertEqual(db1.name, 'Device Bay 1')
self.assertEqual(device_type.inventoryitemtemplates.count(), 3)
ii1 = InventoryItemTemplate.objects.first()
self.assertEqual(ii1.name, 'Inventory Item 1')
def test_export_objects(self): def test_export_objects(self):
url = reverse('dcim:devicetype_list') url = reverse('dcim:devicetype_list')
self.add_permissions('dcim.view_devicetype') self.add_permissions('dcim.view_devicetype')
@ -1393,6 +1419,48 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
} }
class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = InventoryItemTemplate
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
)
Manufacturer.objects.bulk_create(manufacturers)
devicetypes = (
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
inventory_item_templates = (
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 3', manufacturer=manufacturers[0]),
)
for item in inventory_item_templates:
item.save()
cls.form_data = {
'device_type': devicetypes[1].pk,
'name': 'Inventory Item Template X',
'manufacturer': manufacturers[1].pk,
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Inventory Item Template [4-6]',
'manufacturer': manufacturers[1].pk,
}
cls.bulk_edit_data = {
'description': 'Foo bar',
}
class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = DeviceRole model = DeviceRole

View File

@ -115,6 +115,7 @@ urlpatterns = [
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'), path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'), path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'), path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/inventory-items/', views.DeviceTypeInventoryItemsView.as_view(), name='devicetype_inventoryitems'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
@ -203,7 +204,7 @@ urlpatterns = [
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'), path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
# Device bay templates # Module bay templates
path('module-bay-templates/add/', views.ModuleBayTemplateCreateView.as_view(), name='modulebaytemplate_add'), path('module-bay-templates/add/', views.ModuleBayTemplateCreateView.as_view(), name='modulebaytemplate_add'),
path('module-bay-templates/edit/', views.ModuleBayTemplateBulkEditView.as_view(), name='modulebaytemplate_bulk_edit'), path('module-bay-templates/edit/', views.ModuleBayTemplateBulkEditView.as_view(), name='modulebaytemplate_bulk_edit'),
path('module-bay-templates/rename/', views.ModuleBayTemplateBulkRenameView.as_view(), name='modulebaytemplate_bulk_rename'), path('module-bay-templates/rename/', views.ModuleBayTemplateBulkRenameView.as_view(), name='modulebaytemplate_bulk_rename'),
@ -211,6 +212,14 @@ urlpatterns = [
path('module-bay-templates/<int:pk>/edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'), path('module-bay-templates/<int:pk>/edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'),
path('module-bay-templates/<int:pk>/delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'), path('module-bay-templates/<int:pk>/delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'),
# Inventory item templates
path('inventory-item-templates/add/', views.InventoryItemTemplateCreateView.as_view(), name='inventoryitemtemplate_add'),
path('inventory-item-templates/edit/', views.InventoryItemTemplateBulkEditView.as_view(), name='inventoryitemtemplate_bulk_edit'),
path('inventory-item-templates/rename/', views.InventoryItemTemplateBulkRenameView.as_view(), name='inventoryitemtemplate_bulk_rename'),
path('inventory-item-templates/delete/', views.InventoryItemTemplateBulkDeleteView.as_view(), name='inventoryitemtemplate_bulk_delete'),
path('inventory-item-templates/<int:pk>/edit/', views.InventoryItemTemplateEditView.as_view(), name='inventoryitemtemplate_edit'),
path('inventory-item-templates/<int:pk>/delete/', views.InventoryItemTemplateDeleteView.as_view(), name='inventoryitemtemplate_delete'),
# Device roles # Device roles
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'), path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),

View File

@ -869,6 +869,13 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_devicebays' viewname = 'dcim:devicetype_devicebays'
class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
child_model = InventoryItemTemplate
table = tables.InventoryItemTemplateTable
filterset = filtersets.InventoryItemTemplateFilterSet
viewname = 'dcim:devicetype_inventoryitems'
class DeviceTypeEditView(generic.ObjectEditView): class DeviceTypeEditView(generic.ObjectEditView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
model_form = forms.DeviceTypeForm model_form = forms.DeviceTypeForm
@ -890,6 +897,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
'dcim.add_rearporttemplate', 'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate', 'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate', 'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
] ]
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
model_form = forms.DeviceTypeImportForm model_form = forms.DeviceTypeImportForm
@ -903,6 +911,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
('front-ports', forms.FrontPortTemplateImportForm), ('front-ports', forms.FrontPortTemplateImportForm),
('module-bays', forms.ModuleBayTemplateImportForm), ('module-bays', forms.ModuleBayTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm), ('device-bays', forms.DeviceBayTemplateImportForm),
('inventory-items', forms.InventoryItemTemplateImportForm),
)) ))
def prep_related_object_data(self, parent, data): def prep_related_object_data(self, parent, data):
@ -1362,6 +1371,51 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceBayTemplateTable table = tables.DeviceBayTemplateTable
#
# Inventory item templates
#
class InventoryItemTemplateCreateView(generic.ComponentCreateView):
queryset = InventoryItemTemplate.objects.all()
form = forms.DeviceTypeComponentCreateForm
model_form = forms.InventoryItemTemplateForm
def alter_object(self, instance, request):
# Set component (if any)
component_type = request.GET.get('component_type')
component_id = request.GET.get('component_id')
if component_type and component_id:
content_type = get_object_or_404(ContentType, pk=component_type)
instance.component = get_object_or_404(content_type.model_class(), pk=component_id)
return instance
class InventoryItemTemplateEditView(generic.ObjectEditView):
queryset = InventoryItemTemplate.objects.all()
model_form = forms.InventoryItemTemplateForm
class InventoryItemTemplateDeleteView(generic.ObjectDeleteView):
queryset = InventoryItemTemplate.objects.all()
class InventoryItemTemplateBulkEditView(generic.BulkEditView):
queryset = InventoryItemTemplate.objects.all()
table = tables.InventoryItemTemplateTable
form = forms.InventoryItemTemplateBulkEditForm
class InventoryItemTemplateBulkRenameView(generic.BulkRenameView):
queryset = InventoryItemTemplate.objects.all()
class InventoryItemTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItemTemplate.objects.all()
table = tables.InventoryItemTemplateTable
# #
# Device roles # Device roles
# #

View File

@ -44,6 +44,9 @@
{% if perms.dcim.add_devicebaytemplate %} {% if perms.dcim.add_devicebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays</a></li> <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_inventoryitemtemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_inventoryitems' pk=object.pk %}">Inventory Items</a></li>
{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -127,4 +130,12 @@
</li> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with tab_name='inventory-item-templates' inventoryitem_count=object.inventoryitemtemplates.count %}
{% if active_tab == tab_name or inventoryitem_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_inventoryitems' pk=object.pk %}">Inventory Items {% badge inventoryitem_count %}</a>
</li>
{% endif %}
{% endwith %}
{% endblock %} {% endblock %}