From 80943c7ab9e07570be44d5e384917ddc9e04c4e1 Mon Sep 17 00:00:00 2001 From: Naveen Mereddi Date: Fri, 3 Apr 2020 11:11:32 -0500 Subject: [PATCH] Changes for JIRA-3333 --- netbox/dcim/api/nested_serializers.py | 13 +- netbox/dcim/api/serializers.py | 38 +- netbox/dcim/api/urls.py | 6 + netbox/dcim/api/views.py | 32 +- netbox/dcim/filters.py | 77 +++- netbox/dcim/forms.py | 181 ++++++++-- .../migrations/0100_auto_20200327_0158.py | 66 ++++ .../migrations/0101_auto_20200331_1943.py | 44 +++ .../migrations/0102_auto_20200401_1850.py | 19 + netbox/dcim/models/__init__.py | 90 +++++ netbox/dcim/models/device_components.py | 39 ++- netbox/dcim/tables.py | 69 +++- netbox/dcim/tests/test_api.py | 276 ++++++++++++++- netbox/dcim/tests/test_filters.py | 331 ++++++++++++------ netbox/dcim/tests/test_views.py | 3 - netbox/dcim/urls.py | 26 +- netbox/dcim/views.py | 117 ++++++- netbox/templates/dcim/inc/inventoryitem.html | 2 +- netbox/templates/dcim/inventoryitemtype.html | 89 +++++ .../dcim/inventoryitemtype_edit.html | 22 ++ netbox/templates/inc/nav_menu.html | 19 + scripts/cibuild.sh | 2 +- 22 files changed, 1373 insertions(+), 188 deletions(-) create mode 100644 netbox/dcim/migrations/0100_auto_20200327_0158.py create mode 100644 netbox/dcim/migrations/0101_auto_20200331_1943.py create mode 100644 netbox/dcim/migrations/0102_auto_20200401_1850.py create mode 100644 netbox/templates/dcim/inventoryitemtype.html create mode 100644 netbox/templates/dcim/inventoryitemtype_edit.html diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index bb2d61faa..59f8200e0 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.models import ( - Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, + Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, InventoryItemType, DeviceRole, FrontPort, FrontPortTemplate, Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) @@ -16,6 +16,7 @@ __all__ = [ 'NestedDeviceRoleSerializer', 'NestedDeviceSerializer', 'NestedDeviceTypeSerializer', + 'NestedInventoryItemTypeSerializer', 'NestedFrontPortSerializer', 'NestedFrontPortTemplateSerializer', 'NestedInterfaceSerializer', @@ -112,6 +113,16 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer): fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count'] +class NestedInventoryItemTypeSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtype-detail') + manufacturer = NestedManufacturerSerializer(read_only=True) + instance_count = serializers.IntegerField(read_only=True) + + class Meta: + model = InventoryItemType + fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'instance_count'] + + class NestedPowerPortTemplateSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5483904f5..e69426f86 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -9,7 +9,7 @@ from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + Manufacturer, InventoryItem, InventoryItemRole, InventoryItemType, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) @@ -332,10 +332,41 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer): fields = ['id', 'device_type', 'name'] +# +# Inventory Item Role +# + +class InventoryItemRoleSerializer(ValidatedModelSerializer): + inventoryitem_count = serializers.IntegerField(read_only=True) + + class Meta: + model = InventoryItemRole + fields = [ + 'id', 'name', 'slug', 'inventoryitem_count' + ] + +# +# Inventory Item Type +# + + +class InventoryItemTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): + manufacturer = NestedManufacturerSerializer() + instance_count = serializers.IntegerField(read_only=True) + tags = TagListSerializerField(required=False) + + class Meta: + model = InventoryItemType + fields = [ + 'id', 'manufacturer', 'model', 'slug', 'part_number', 'tags', 'created', + 'last_updated', 'instance_count', + ] + # # Devices # + class DeviceRoleSerializer(ValidatedModelSerializer): device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) @@ -612,14 +643,13 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() # 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) tags = TagListSerializerField(required=False) class Meta: model = InventoryItem fields = [ - 'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', - 'description', 'tags', + 'id', 'device', 'parent', 'name', 'part_id', 'serial', 'asset_tag', 'discovered', + 'description', 'tags', 'role', 'type', 'site', ] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 5a915becc..8dee38cfb 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -65,6 +65,12 @@ router.register('interface-connections', views.InterfaceConnectionViewSet, basen # Cables router.register('cables', views.CableViewSet) +# Inventory Item Roles +router.register('inventory-item-roles', views.InventoryItemRoleViewSet) + +# Inventory Item types +router.register('inventory-item-types', views.InventoryItemTypeViewSet) + # Virtual chassis router.register('virtual-chassis', views.VirtualChassisViewSet) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 34ba0c47d..ece2386f9 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -17,7 +17,7 @@ from dcim import filters from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + Manufacturer, InventoryItem, InventoryItemRole, InventoryItemType, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) @@ -269,7 +269,7 @@ class RackReservationViewSet(ModelViewSet): class ManufacturerViewSet(ModelViewSet): queryset = Manufacturer.objects.annotate( devicetype_count=get_subquery(DeviceType, 'manufacturer'), - inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'), + inventoryitem_count=get_subquery(InventoryItem, 'type__manufacturer'), platform_count=get_subquery(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer @@ -352,6 +352,32 @@ class DeviceRoleViewSet(ModelViewSet): serializer_class = serializers.DeviceRoleSerializer filterset_class = filters.DeviceRoleFilterSet +# +# Inventory item roles +# + + +class InventoryItemRoleViewSet(ModelViewSet): + + queryset = InventoryItemRole.objects.annotate( + inventoryitem_count=get_subquery(InventoryItem, 'role'), + ) + + serializer_class = serializers.InventoryItemRoleSerializer + filterset_class = filters.InventoryItemRoleFilterSet + +# +# Inventory Item types +# + + +class InventoryItemTypeViewSet(ModelViewSet): + queryset = InventoryItemType.objects.prefetch_related('manufacturer').prefetch_related('tags').annotate( + instance_count=Count('instances') + ) + serializer_class = serializers.InventoryItemTypeSerializer + filterset_class = filters.InventoryItemTypeFilterSet + # # Platforms @@ -576,7 +602,7 @@ class DeviceBayViewSet(ModelViewSet): class InventoryItemViewSet(ModelViewSet): - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') + queryset = InventoryItem.objects.prefetch_related('device', 'type__manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer filterset_class = filters.InventoryItemFilterSet diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 7b98359c8..5cd0aa27c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -15,9 +15,9 @@ from .constants import * from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, - VirtualChassis, + InventoryItem, InventoryItemRole, InventoryItemType, Manufacturer, Platform, PowerFeed, PowerOutlet, + PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, + RearPortTemplate, Region, Site, VirtualChassis, ) @@ -39,6 +39,8 @@ __all__ = ( 'InterfaceFilterSet', 'InterfaceTemplateFilterSet', 'InventoryItemFilterSet', + 'InventoryItemRoleFilterSet', + 'InventoryItemTypeFilterSet', 'ManufacturerFilterSet', 'PlatformFilterSet', 'PowerConnectionFilterSet', @@ -479,6 +481,13 @@ class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'color', 'vm_role'] +class InventoryItemRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): + + class Meta: + model = InventoryItemRole + fields = ['id', 'name', 'slug'] + + class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', @@ -989,15 +998,25 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): queryset=InventoryItem.objects.all(), label='Parent inventory item (ID)', ) - manufacturer_id = django_filters.ModelMultipleChoiceFilter( - queryset=Manufacturer.objects.all(), - label='Manufacturer (ID)', + role_id = django_filters.ModelMultipleChoiceFilter( + queryset=InventoryItemRole.objects.all(), + label='Inventory Item Role (ID)' ) - manufacturer = django_filters.ModelMultipleChoiceFilter( - field_name='manufacturer__slug', - queryset=Manufacturer.objects.all(), + role = django_filters.ModelMultipleChoiceFilter( + field_name='role__slug', + queryset=InventoryItemRole.objects.all(), + to_field_name="slug", + label="Inventory Item Role (slug)" + ) + type_id = django_filters.ModelMultipleChoiceFilter( + queryset=InventoryItemType.objects.all(), + label='Inventory Item Type (ID)' + ) + type = django_filters.ModelMultipleChoiceFilter( + field_name='type__slug', + queryset=InventoryItemType.objects.all(), to_field_name='slug', - label='Manufacturer (slug)', + label='Inventory Item Type (slug)' ) serial = django_filters.CharFilter( lookup_expr='iexact' @@ -1020,6 +1039,44 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): return queryset.filter(qs_filter) +class InventoryItemTypeFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) + q = django_filters.CharFilter( + method='search', + label='Search', + ) + 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)', + ) + tag = TagFilter() + + class Meta: + model = InventoryItemType + fields = [ + 'model', 'slug', 'part_number' + ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(manufacturer__name__icontains=value) | + Q(model__icontains=value) | + Q(part_number__icontains=value) | + Q(slug__icontains=value) + ) + + class VirtualChassisFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 311a8e69e..e0e8d819a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -32,8 +32,9 @@ from .constants import * from .models import ( Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer, - InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, - Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, + InventoryItem, InventoryItemRole, InventoryItemType, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, + PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, + Region, Site, VirtualChassis, ) DEVICE_BY_PK_RE = r'{\d+\}' @@ -58,7 +59,6 @@ def get_device_by_name_or_pk(name): class DeviceComponentFilterForm(BootstrapMixin, forms.Form): - field_order = [ 'q', 'region', 'site' ] @@ -923,7 +923,6 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): class ManufacturerCSVForm(forms.ModelForm): - class Meta: model = Manufacturer fields = Manufacturer.csv_headers @@ -1065,7 +1064,6 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): # class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = ConsolePortTemplate fields = [ @@ -1105,7 +1103,6 @@ class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = ConsoleServerPortTemplate fields = [ @@ -1145,7 +1142,6 @@ class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = PowerPortTemplate fields = [ @@ -1205,7 +1201,6 @@ class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = PowerOutletTemplate fields = [ @@ -1216,7 +1211,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): } def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) # Limit power_port choices to current DeviceType @@ -1280,7 +1274,6 @@ class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = InterfaceTemplate fields = [ @@ -1330,7 +1323,6 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = FrontPortTemplate fields = [ @@ -1342,7 +1334,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): } def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) # Limit rear_port choices to current DeviceType @@ -1431,7 +1422,6 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = RearPortTemplate fields = [ @@ -1478,7 +1468,6 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): - class Meta: model = DeviceBayTemplate fields = [ @@ -1537,7 +1526,6 @@ class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ConsolePortTemplateImportForm(ComponentTemplateImportForm): - class Meta: model = ConsolePortTemplate fields = [ @@ -1546,7 +1534,6 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm): class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): - class Meta: model = ConsoleServerPortTemplate fields = [ @@ -1555,7 +1542,6 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): class PowerPortTemplateImportForm(ComponentTemplateImportForm): - class Meta: model = PowerPortTemplate fields = [ @@ -1619,7 +1605,6 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm): class DeviceBayTemplateImportForm(ComponentTemplateImportForm): - class Meta: model = DeviceBayTemplate fields = [ @@ -1814,7 +1799,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']: device_type_id = kwargs['initial']['device_type'] - manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first() + manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', + flat=True).first() kwargs['initial']['manufacturer'] = manufacturer_id if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']: @@ -3671,7 +3657,6 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): class CableForm(BootstrapMixin, forms.ModelForm): - class Meta: model = Cable fields = [ @@ -3685,7 +3670,6 @@ class CableForm(BootstrapMixin, forms.ModelForm): class CableCSVForm(forms.ModelForm): - # Termination A side_a_device = FlexibleModelChoiceField( queryset=Device.objects.all(), @@ -3853,7 +3837,6 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): ] def clean(self): - # Validate length/unit length = self.cleaned_data.get('length') length_unit = self.cleaned_data.get('length_unit') @@ -3970,7 +3953,6 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ) def __init__(self, device_bay, *args, **kwargs): - super().__init__(*args, **kwargs) self.fields['installed_device'].queryset = Device.objects.filter( @@ -4187,6 +4169,30 @@ class InventoryItemCSVForm(forms.ModelForm): 'invalid_choice': 'Invalid manufacturer.', } ) + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + required=False, + error_messages={ + "invalid_choice": 'Invalid site.', + } + ) + role = forms.ModelChoiceField( + queryset=InventoryItemRole.objects.all(), + to_field_name='name', + required=False, + error_messages={ + 'invalid_choice': 'Invalid item role.', + } + ) + type = forms.ModelChoiceField( + queryset=InventoryItemType.objects.all(), + to_field_name='model', + required=False, + error_messages={ + 'invalid_choice': 'Invalie item type.', + } + ) class Meta: model = InventoryItem @@ -4202,10 +4208,6 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): queryset=Device.objects.all(), required=False ) - manufacturer = DynamicModelChoiceField( - queryset=Manufacturer.objects.all(), - required=False - ) part_id = forms.CharField( max_length=50, required=False, @@ -4215,10 +4217,25 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): max_length=100, required=False ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/sites" + ) + ) + role = DynamicModelChoiceField( + queryset=InventoryItemRole.objects.all(), + required=False + ) + type = DynamicModelChoiceField( + queryset=InventoryItemType.objects.all(), + required=False + ) class Meta: nullable_fields = [ - 'manufacturer', 'part_id', 'description', + 'part_id', 'description', 'site', 'role', 'type', ] @@ -4263,6 +4280,16 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): value_field="slug", ) ) + + role = DynamicModelMultipleChoiceField( + queryset=InventoryItemRole.objects.all(), + to_field_name='slug', + required=False, + widget=APISelect( + api_url="/api/dcim/inventory-item-roles/", + value_field="slug", + ) + ) discovered = forms.NullBooleanField( required=False, widget=StaticSelect2( @@ -4271,11 +4298,109 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): ) tag = TagFilterField(model) +# +# Inventory Item types +# + + +class InventoryItemTypeForm(BootstrapMixin, CustomFieldModelForm): + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + widget=APISelect( + api_url="/api/dcim/manufacturers/", + ) + ) + slug = SlugField( + slug_source='model' + ) + tags = TagField( + required=False + ) + + class Meta: + model = InventoryItemType + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', 'tags', + ] + + +class InventoryItemTypeImportForm(BootstrapMixin, forms.ModelForm): + manufacturer = forms.ModelChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='name' + ) + + class Meta: + model = InventoryItemType + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', + ] + + +class InventoryItemTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=InventoryItemType.objects.all(), + widget=forms.MultipleHiddenInput() + ) + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers" + ) + ) + + class Meta: + nullable_fields = [] + + +class InventoryItemTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = InventoryItemType + q = forms.CharField( + required=False, + label='Search' + ) + manufacturer = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/manufacturers/", + value_field="slug", + ) + ) + tag = TagFilterField(model) + +# +# Inventory Item Role +# + + +class InventoryItemRoleCSVForm(forms.ModelForm): + slug = SlugField() + + class Meta: + model = InventoryItemRole + fields = InventoryItemRole.csv_headers + help_texts = { + 'name': 'Name of inventory item role', + } + + +class InventoryItemRoleForm(BootstrapMixin, forms.ModelForm): + slug = SlugField() + + class Meta: + model = InventoryItemRole + fields = [ + 'name', 'slug', + ] # # Virtual chassis # + class DeviceSelectionForm(forms.Form): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), diff --git a/netbox/dcim/migrations/0100_auto_20200327_0158.py b/netbox/dcim/migrations/0100_auto_20200327_0158.py new file mode 100644 index 000000000..0c7558fd1 --- /dev/null +++ b/netbox/dcim/migrations/0100_auto_20200327_0158.py @@ -0,0 +1,66 @@ +# Generated by Django 2.2.10 on 2020-03-27 01:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0099_powerfeed_negative_voltage'), + ] + + operations = [ + migrations.CreateModel( + name='InventoryItemRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.RemoveField( + model_name='inventoryitem', + name='manufacturer', + ), + migrations.AddField( + model_name='inventoryitem', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Site'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='device', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.Device'), + ), + migrations.CreateModel( + name='InventoryItemType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('model', models.CharField(max_length=50)), + ('part_number', models.CharField(blank=True, max_length=50)), + ('slug', models.SlugField(unique=True)), + ('manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='item_types', to='dcim.Manufacturer')), + ], + options={ + 'ordering': ['model'], + }, + ), + migrations.AddField( + model_name='inventoryitem', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.InventoryItemRole'), + ), + migrations.AddField( + model_name='inventoryitem', + name='type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='dcim.InventoryItemType'), + ), + ] diff --git a/netbox/dcim/migrations/0101_auto_20200331_1943.py b/netbox/dcim/migrations/0101_auto_20200331_1943.py new file mode 100644 index 000000000..6063a4858 --- /dev/null +++ b/netbox/dcim/migrations/0101_auto_20200331_1943.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.10 on 2020-03-31 19:43 + +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0038_webhook_template_support'), + ('dcim', '0100_auto_20200327_0158'), + ] + + operations = [ + migrations.AlterModelOptions( + name='inventoryitemtype', + options={'ordering': ['manufacturer', 'model']}, + ), + migrations.AddField( + model_name='inventoryitemtype', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.InventoryItemType'), + ), + migrations.AlterField( + model_name='inventoryitemtype', + name='manufacturer', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inventory_item_types', to='dcim.Manufacturer'), + ), + migrations.AlterField( + model_name='inventoryitemtype', + name='slug', + field=models.SlugField(), + ), + migrations.AlterUniqueTogether( + name='inventoryitemtype', + unique_together={('manufacturer', 'slug'), ('manufacturer', 'model')}, + ), + ] diff --git a/netbox/dcim/migrations/0102_auto_20200401_1850.py b/netbox/dcim/migrations/0102_auto_20200401_1850.py new file mode 100644 index 000000000..67db3434a --- /dev/null +++ b/netbox/dcim/migrations/0102_auto_20200401_1850.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.11 on 2020-04-01 18:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0101_auto_20200331_1943'), + ] + + operations = [ + migrations.AlterField( + model_name='inventoryitem', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inventoryitems', to='dcim.InventoryItemRole'), + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 63a320c78..abf338b4c 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -52,6 +52,8 @@ __all__ = ( 'Interface', 'InterfaceTemplate', 'InventoryItem', + 'InventoryItemRole', + 'InventoryItemType', 'Manufacturer', 'Platform', 'PowerFeed', @@ -2179,3 +2181,91 @@ class Cable(ChangeLoggedModel): b_endpoint = b_path[-1][2] return a_endpoint, b_endpoint, path_status + +# +# Inventory Item Role +# + + +class InventoryItemRole(ChangeLoggedModel): + """ + Inventory Item Role + """ + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + + csv_headers = ['name', 'slug'] + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + def to_csv(self): + return ( + self.name, + self.slug, + ) + + +class InventoryItemType(ChangeLoggedModel): + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='inventory_item_types' + ) + model = models.CharField( + max_length=50 + ) + slug = models.SlugField() + part_number = models.CharField( + max_length=50, + blank=True, + help_text='Discrete part number (optional)' + ) + + tags = TaggableManager(through=TaggedItem) + + clone_fields = [ + 'manufacturer' + ] + + class Meta: + ordering = ['manufacturer', 'model'] + unique_together = [ + ['manufacturer', 'model'], + ['manufacturer', 'slug'], + ] + + def __str__(self): + return self.model + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_absolute_url(self): + return reverse('dcim:inventoryitemtype', args=[self.pk]) + + def to_yaml(self): + data = OrderedDict(( + ('manufacturer', self.manufacturer.name), + ('model', self.model), + ('slug', self.slug), + ('part_number', self.part_number), + )) + + return yaml.dump(dict(data), sort_keys=False) + + def save(self, *args, **kwargs): + ret = super().save(*args, **kwargs) + + return ret + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4ccfaf808..644f51902 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1027,8 +1027,28 @@ class InventoryItem(ComponentModel): """ device = models.ForeignKey( to='dcim.Device', - on_delete=models.CASCADE, - related_name='inventory_items' + on_delete=models.SET_NULL, + null=True + ) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + null=True, + blank=True + ) + role = models.ForeignKey( + to='dcim.InventoryItemRole', + on_delete=models.PROTECT, + related_name='inventoryitems', + null=True, + blank=True + ) + type = models.ForeignKey( + to='dcim.InventoryItemType', + on_delete=models.PROTECT, + related_name='instances', + null=True, + blank=True ) parent = models.ForeignKey( to='self', @@ -1046,13 +1066,6 @@ class InventoryItem(ComponentModel): max_length=100, blank=True ) - manufacturer = models.ForeignKey( - to='dcim.Manufacturer', - on_delete=models.PROTECT, - related_name='inventory_items', - blank=True, - null=True - ) part_id = models.CharField( max_length=50, verbose_name='Part ID', @@ -1079,7 +1092,7 @@ class InventoryItem(ComponentModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', + 'device', 'name', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', ] class Meta: @@ -1090,13 +1103,15 @@ class InventoryItem(ComponentModel): return self.name def get_absolute_url(self): - return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk}) + return reverse('dcim:inventoryitem_list') def to_csv(self): return ( self.device.name or '{{{}}}'.format(self.device.pk), self.name, - self.manufacturer.name if self.manufacturer else None, + self.site, + self.role, + self.type, self.part_id, self.serial, self.asset_tag, diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index e3675ae97..2cd838bf6 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -6,7 +6,7 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + InventoryItem, InventoryItemRole, InventoryItemType, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) @@ -112,6 +112,20 @@ DEVICEROLE_ACTIONS = """ {% endif %} """ +INVENTORYITEMROLE_ACTIONS = """ + + + + +{% if perms.dcim.change_inventoryitemrole %} + +{% endif %} +""" + +INVENTORYITEMROLE_INVENTORYITEM_COUNT = """ +{{ value }} +""" + DEVICEROLE_DEVICE_COUNT = """ {{ value }} """ @@ -160,6 +174,10 @@ DEVICETYPE_INSTANCES_TEMPLATE = """ {{ record.instance_count }} """ +INVENTORYITEMTYPE_INSTANCES_TEMPLATE = """ +{{ record.instance_count }} +""" + UTILIZATION_GRAPH = """ {% load helpers %} {% utilization_graph value %} @@ -1031,16 +1049,65 @@ class InventoryItemTable(BaseTable): pk = ToggleColumn() device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')]) manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer') + role = tables.Column(accessor=Accessor('role.name'), verbose_name='Role') class Meta(BaseTable.Meta): model = InventoryItem fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description') +# +# InventoryItemRoles +# + + +class InventoryItemRoleTable(BaseTable): + pk = ToggleColumn() + slug = tables.Column(verbose_name='Slug') + inventoryitem_count = tables.TemplateColumn( + template_code=INVENTORYITEMROLE_INVENTORYITEM_COUNT, + accessor=Accessor('inventoryitems.count'), + orderable=False, + verbose_name='Inventory Items' + ) + actions = tables.TemplateColumn( + template_code=INVENTORYITEMROLE_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) + + class Meta(BaseTable.Meta): + model = InventoryItemRole + fields = ('pk', 'name', 'slug', 'inventoryitem_count') + +# +# Inventory Item types +# + + +class InventoryItemTypeTable(BaseTable): + pk = ToggleColumn() + model = tables.LinkColumn( + viewname='dcim:inventoryitemtype', + args=[Accessor('pk')], + verbose_name='Inventory Item Type' + ) + instance_count = tables.TemplateColumn( + template_code=INVENTORYITEMTYPE_INSTANCES_TEMPLATE, + verbose_name='Instances' + ) + + class Meta(BaseTable.Meta): + model = InventoryItemType + fields = ( + 'pk', 'model', 'manufacturer', 'part_number', 'slug', + 'instance_count', + ) # # Virtual chassis # + class VirtualChassisTable(BaseTable): pk = ToggleColumn() master = tables.LinkColumn() diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 09de27d92..3906f6cfc 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -6,13 +6,13 @@ from rest_framework import status from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.api import serializers from dcim.choices import * -from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, Interface, InterfaceTemplate, Manufacturer, - InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel, - Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, + InventoryItem, InventoryItemRole, InventoryItemType, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, + PowerOutletTemplate, PowerPanel, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, ) +from dcim.constants import * from ipam.models import IPAddress, VLAN from extras.models import Graph from utilities.testing import APITestCase, choices_to_dict @@ -105,7 +105,6 @@ class AppTest(APITestCase): class RegionTest(APITestCase): def setUp(self): - super().setUp() self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1') @@ -113,7 +112,6 @@ class RegionTest(APITestCase): self.region3 = Region.objects.create(name='Test Region 3', slug='test-region-3') def test_get_region(self): - url = reverse('dcim-api:region-detail', kwargs={'pk': self.region1.pk}) response = self.client.get(url, **self.header) @@ -3249,7 +3247,7 @@ class InventoryItemTest(APITestCase): super().setUp() - site = Site.objects.create(name='Test Site 1', slug='test-site-1') + self.site = Site.objects.create(name='Test Site 1', slug='test-site-1') self.manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') devicetype = DeviceType.objects.create( manufacturer=self.manufacturer, model='Test Device Type 1', slug='test-device-type-1' @@ -3258,8 +3256,16 @@ class InventoryItemTest(APITestCase): name='Test Device Role 1', slug='test-device-role-1', color='ff0000' ) self.device = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site + device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site ) + + self.inventoryitemrole = InventoryItemRole.objects.create( + name='Inventory Item Role 1', slug='inventory-item-role-1' + ) + self.inventoryitemtype = InventoryItemType.objects.create( + model='Inventory Item 1', manufacturer=self.manufacturer, slug='inventory-item-type-1' + ) + self.inventoryitem1 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 1') self.inventoryitem2 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 2') self.inventoryitem3 = InventoryItem.objects.create(device=self.device, name='Test Inventory Item 3') @@ -3282,9 +3288,11 @@ class InventoryItemTest(APITestCase): data = { 'device': self.device.pk, + 'site': self.site.pk, + 'type': self.inventoryitemtype.pk, + 'role': self.inventoryitemrole.pk, 'parent': self.inventoryitem1.pk, 'name': 'Test Inventory Item 4', - 'manufacturer': self.manufacturer.pk, } url = reverse('dcim-api:inventoryitem-list') @@ -3296,28 +3304,36 @@ class InventoryItemTest(APITestCase): self.assertEqual(inventoryitem4.device_id, data['device']) self.assertEqual(inventoryitem4.parent_id, data['parent']) self.assertEqual(inventoryitem4.name, data['name']) - self.assertEqual(inventoryitem4.manufacturer_id, data['manufacturer']) + self.assertEqual(inventoryitem4.site_id, data['site']) + self.assertEqual(inventoryitem4.type_id, data['type']) + self.assertEqual(inventoryitem4.role_id, data['role']) def test_create_inventoryitem_bulk(self): data = [ { 'device': self.device.pk, + 'site': self.site.pk, + 'type': self.inventoryitemtype.pk, + 'role': self.inventoryitemrole.pk, 'parent': self.inventoryitem1.pk, 'name': 'Test Inventory Item 4', - 'manufacturer': self.manufacturer.pk, }, { 'device': self.device.pk, + 'site': self.site.pk, + 'type': self.inventoryitemtype.pk, + 'role': self.inventoryitemrole.pk, 'parent': self.inventoryitem1.pk, 'name': 'Test Inventory Item 5', - 'manufacturer': self.manufacturer.pk, }, { 'device': self.device.pk, + 'site': self.site.pk, + 'type': self.inventoryitemtype.pk, + 'role': self.inventoryitemrole.pk, 'parent': self.inventoryitem1.pk, 'name': 'Test Inventory Item 6', - 'manufacturer': self.manufacturer.pk, }, ] @@ -3334,9 +3350,11 @@ class InventoryItemTest(APITestCase): data = { 'device': self.device.pk, + 'site': self.site.pk, + 'type': self.inventoryitemtype.pk, + 'role': self.inventoryitemrole.pk, 'parent': self.inventoryitem1.pk, 'name': 'Test Inventory Item X', - 'manufacturer': self.manufacturer.pk, } url = reverse('dcim-api:inventoryitem-detail', kwargs={'pk': self.inventoryitem1.pk}) @@ -3348,7 +3366,6 @@ class InventoryItemTest(APITestCase): self.assertEqual(inventoryitem1.device_id, data['device']) self.assertEqual(inventoryitem1.parent_id, data['parent']) self.assertEqual(inventoryitem1.name, data['name']) - self.assertEqual(inventoryitem1.manufacturer_id, data['manufacturer']) def test_delete_inventoryitem(self): @@ -4329,3 +4346,234 @@ class PowerFeedTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(PowerFeed.objects.count(), 5) + + +class InventoryItemRoleTest(APITestCase): + + def setUp(self): + + super().setUp() + + self.inventoryitemrole1 = InventoryItemRole.objects.create( + name='Test Inventory Item Role 1', slug='test-inventory-item-role-1' + ) + self.inventoryitem1 = InventoryItem.objects.create(name='Test Inventory Item 1', role=self.inventoryitemrole1) + + self.inventoryitemrole2 = InventoryItemRole.objects.create( + name='Test Inventory Item Role 2', slug='test-inventory-item-role-2' + ) + self.inventoryitemrole3 = InventoryItemRole.objects.create( + name='Test Inventory Item Role 3', slug='test-inventory-item-role-3' + ) + + def test_get_inventoryitemrole(self): + + url = reverse('dcim-api:inventoryitemrole-detail', kwargs={'pk': self.inventoryitemrole1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.inventoryitemrole1.name) + + def test_list_inventoryitemroles(self): + + url = reverse('dcim-api:inventoryitemrole-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_list_inventoryitemroles_brief(self): + + url = reverse('dcim-api:inventoryitemrole-list') + response = self.client.get('{}?brief=1'.format(url), **self.header) + + self.assertEqual( + sorted(response.data['results'][0]), + ['id', 'inventoryitem_count', 'name', 'slug', ] + ) + + def test_create_inventoryitemrole(self): + + data = { + 'name': 'Test Inventory Item Role 4', + 'slug': 'test-inventoryitem-role-4', + } + + url = reverse('dcim-api:inventoryitemrole-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(InventoryItemRole.objects.count(), 4) + inventoryitemrole4 = InventoryItemRole.objects.get(pk=response.data['id']) + self.assertEqual(inventoryitemrole4.name, data['name']) + self.assertEqual(inventoryitemrole4.slug, data['slug']) + + def test_create_inventoryitemrole_bulk(self): + + data = [ + { + 'name': 'Test Inventory Item Role 4', + 'slug': 'test-inventoryitem-role-4', + }, + { + 'name': 'Test Inventory Item Role 5', + 'slug': 'test-inventoryitem-role-5', + }, + { + 'name': 'Test Inventory Item Role 6', + 'slug': 'test-inventoryitem-role-6', + }, + ] + + url = reverse('dcim-api:inventoryitemrole-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(InventoryItemRole.objects.count(), 6) + self.assertEqual(response.data[0]['name'], data[0]['name']) + self.assertEqual(response.data[1]['name'], data[1]['name']) + self.assertEqual(response.data[2]['name'], data[2]['name']) + + def test_update_inventoryitemrole(self): + + data = { + 'name': 'Test Inventory Item Role X', + 'slug': 'test-inventoryitem-role-x', + } + + url = reverse('dcim-api:inventoryitemrole-detail', kwargs={'pk': self.inventoryitemrole1.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(InventoryItemRole.objects.count(), 3) + inventoryitemrole1 = InventoryItemRole.objects.get(pk=response.data['id']) + self.assertEqual(inventoryitemrole1.name, data['name']) + self.assertEqual(inventoryitemrole1.slug, data['slug']) + + def test_delete_inventoryitemrole(self): + url = reverse('dcim-api:inventoryitemrole-detail', kwargs={'pk': self.inventoryitemrole2.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(InventoryItemRole.objects.count(), 2) + + def test_delete_inventoryitemrole_with_items(self): + url = reverse('dcim-api:inventoryitemrole-detail', kwargs={'pk': self.inventoryitemrole1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_409_CONFLICT) + self.assertEqual(InventoryItemRole.objects.count(), 3) + + +class InventoryItemTypeTest(APITestCase): + + def setUp(self): + + super().setUp() + + self.manufacturer1 = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') + self.manufacturer2 = Manufacturer.objects.create(name='Test Manufacturer 2', slug='test-manufacturer-2') + self.inventoryitemtype1 = InventoryItemType.objects.create( + manufacturer=self.manufacturer1, model='Test Inventory Item Type 1', slug='test-inventory-item-type-1' + ) + self.inventoryitemtype2 = InventoryItemType.objects.create( + manufacturer=self.manufacturer1, model='Test Inventory Item Type 2', slug='test-inventory-item-type-2' + ) + self.inventoryitemtype3 = InventoryItemType.objects.create( + manufacturer=self.manufacturer1, model='Test Inventory Item Type 3', slug='test-inventory-item-type-3' + ) + + def test_get_inventoryitemtype(self): + + url = reverse('dcim-api:inventoryitemtype-detail', kwargs={'pk': self.inventoryitemtype1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['model'], self.inventoryitemtype1.model) + + def test_list_inventoryitemtypes(self): + + url = reverse('dcim-api:inventoryitemtype-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_list_inventoryitemtypes_brief(self): + + url = reverse('dcim-api:inventoryitemtype-list') + response = self.client.get('{}?brief=1'.format(url), **self.header) + + self.assertEqual( + sorted(response.data['results'][0]), + ['id', 'instance_count', 'manufacturer', 'model', 'slug', 'url'] + ) + + def test_create_inventoryitemtype(self): + + data = { + 'manufacturer': self.manufacturer1.pk, + 'model': 'Test Inventory Item Type 4', + 'slug': 'test-inventory-item-type-4', + } + + url = reverse('dcim-api:inventoryitemtype-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(InventoryItemType.objects.count(), 4) + inventoryitemtype4 = InventoryItemType.objects.get(pk=response.data['id']) + self.assertEqual(inventoryitemtype4.manufacturer_id, data['manufacturer']) + self.assertEqual(inventoryitemtype4.model, data['model']) + self.assertEqual(inventoryitemtype4.slug, data['slug']) + + def test_create_inventoryitemtype_bulk(self): + + data = [ + { + 'manufacturer': self.manufacturer1.pk, + 'model': 'Test Inventory Item Type 4', + 'slug': 'test-inventory-item-type-4', + }, + { + 'manufacturer': self.manufacturer1.pk, + 'model': 'Test Inventory Item Type 5', + 'slug': 'test-inventory-item-type-5', + }, + { + 'manufacturer': self.manufacturer1.pk, + 'model': 'Test Inventory Item Type 6', + 'slug': 'test-inventory-item-type-6', + }, + ] + + url = reverse('dcim-api:inventoryitemtype-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(InventoryItemType.objects.count(), 6) + self.assertEqual(response.data[0]['model'], data[0]['model']) + self.assertEqual(response.data[1]['model'], data[1]['model']) + self.assertEqual(response.data[2]['model'], data[2]['model']) + + def test_update_inventoryitemtype(self): + + data = { + 'manufacturer': self.manufacturer2.pk, + 'model': 'Test Inventory Item Type X', + 'slug': 'test-inventory-item-type-x', + } + + url = reverse('dcim-api:inventoryitemtype-detail', kwargs={'pk': self.inventoryitemtype1.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(InventoryItemType.objects.count(), 3) + inventoryitemtype1 = InventoryItemType.objects.get(pk=response.data['id']) + self.assertEqual(inventoryitemtype1.manufacturer_id, data['manufacturer']) + self.assertEqual(inventoryitemtype1.model, data['model']) + self.assertEqual(inventoryitemtype1.slug, data['slug']) + + def test_delete_inventoryitemtype(self): + + url = reverse('dcim-api:inventoryitemtype-detail', kwargs={'pk': self.inventoryitemtype1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(InventoryItemType.objects.count(), 2) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index c93a3a5f6..d04a7e5cf 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -6,9 +6,9 @@ from dcim.filters import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerPortTemplate, PowerOutlet, - PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, - VirtualChassis, + InventoryItem, InventoryItemRole, InventoryItemType, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, + PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, + RearPortTemplate, Region, Site, VirtualChassis, ) from ipam.models import IPAddress from tenancy.models import Tenant, TenantGroup @@ -67,7 +67,6 @@ class SiteTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -91,9 +90,15 @@ class SiteTestCase(TestCase): Tenant.objects.bulk_create(tenants) sites = ( - Site(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'), - Site(name='Site 2', slug='site-2', region=regions[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'), - Site(name='Site 3', slug='site-3', region=regions[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'), + Site(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[0], + status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, + contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'), + Site(name='Site 2', slug='site-2', region=regions[1], tenant=tenants[1], + status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, + contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'), + Site(name='Site 3', slug='site-3', region=regions[2], tenant=tenants[2], + status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, + contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'), ) Site.objects.bulk_create(sites) @@ -175,7 +180,6 @@ class RackGroupTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -232,7 +236,6 @@ class RackRoleTestCase(TestCase): @classmethod def setUpTestData(cls): - rack_roles = ( RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'), RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'), @@ -264,7 +267,6 @@ class RackTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -309,9 +311,18 @@ class RackTestCase(TestCase): Tenant.objects.bulk_create(tenants) racks = ( - Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), - Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), - Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH), + Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], + status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', + type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, + outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], + status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', + type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, + outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], + status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', + type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, + outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH), ) Rack.objects.bulk_create(racks) @@ -429,7 +440,6 @@ class RackReservationTestCase(TestCase): @classmethod def setUpTestData(cls): - sites = ( Site(name='Site 1', slug='site-1'), Site(name='Site 2', slug='site-2'), @@ -527,7 +537,6 @@ class ManufacturerTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -555,7 +564,6 @@ class DeviceTypeTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -564,9 +572,12 @@ class DeviceTypeTestCase(TestCase): Manufacturer.objects.bulk_create(manufacturers) device_types = ( - DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True), - DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), - DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD), + DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', + u_height=1, is_full_depth=True), + DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', + u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), + DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', + u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD), ) DeviceType.objects.bulk_create(device_types) @@ -597,8 +608,10 @@ class DeviceTypeTestCase(TestCase): ) RearPortTemplate.objects.bulk_create(rear_ports) FrontPortTemplate.objects.bulk_create(( - FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), - FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, + rear_port=rear_ports[0]), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, + rear_port=rear_ports[1]), )) DeviceBayTemplate.objects.bulk_create(( DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), @@ -692,7 +705,6 @@ class ConsolePortTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -729,7 +741,6 @@ class ConsoleServerPortTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -766,7 +777,6 @@ class PowerPortTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -811,7 +821,6 @@ class PowerOutletTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -822,9 +831,12 @@ class PowerOutletTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) PowerOutletTemplate.objects.bulk_create(( - PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A), - PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B), - PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C), + PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A), + PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B), + PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C), )) def test_id(self): @@ -853,7 +865,6 @@ class InterfaceTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -864,9 +875,12 @@ class InterfaceTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) InterfaceTemplate.objects.bulk_create(( - InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True), - InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False), - InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), + InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, + mgmt_only=True), + InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, + mgmt_only=False), + InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, + mgmt_only=False), )) def test_id(self): @@ -901,7 +915,6 @@ class FrontPortTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -919,9 +932,12 @@ class FrontPortTemplateTestCase(TestCase): RearPortTemplate.objects.bulk_create(rear_ports) FrontPortTemplate.objects.bulk_create(( - FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C), - FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH), - FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC), + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], + type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], + type=PortTypeChoices.TYPE_110_PUNCH), + FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], + type=PortTypeChoices.TYPE_BNC), )) def test_id(self): @@ -950,7 +966,6 @@ class RearPortTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -961,9 +976,12 @@ class RearPortTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) RearPortTemplate.objects.bulk_create(( - RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1), - RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2), - RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3), + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, + positions=1), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, + positions=2), + RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, + positions=3), )) def test_id(self): @@ -996,7 +1014,6 @@ class DeviceBayTemplateTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_types = ( @@ -1033,7 +1050,6 @@ class DeviceRoleTestCase(TestCase): @classmethod def setUpTestData(cls): - device_roles = ( DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True), DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True), @@ -1071,7 +1087,6 @@ class PlatformTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -1117,7 +1132,6 @@ class DeviceTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -1198,9 +1212,16 @@ class DeviceTestCase(TestCase): Tenant.objects.bulk_create(tenants) devices = ( - Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), - Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]), - Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]), + Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], + tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, + face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], + local_context_data={"foo": 123}), + Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], + tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, + face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]), + Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], + tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, + face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]), ) Device.objects.bulk_create(devices) @@ -1452,7 +1473,6 @@ class ConsolePortTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -1548,7 +1568,6 @@ class ConsoleServerPortTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -1644,7 +1663,6 @@ class PowerPortTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -1678,8 +1696,10 @@ class PowerPortTestCase(TestCase): power_ports = ( PowerPort(device=devices[0], name='Power Port 1', maximum_draw=100, allocated_draw=50, description='First'), - PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, description='Second'), - PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, description='Third'), + PowerPort(device=devices[1], name='Power Port 2', maximum_draw=200, allocated_draw=100, + description='Second'), + PowerPort(device=devices[2], name='Power Port 3', maximum_draw=300, allocated_draw=150, + description='Third'), ) PowerPort.objects.bulk_create(power_ports) @@ -1748,7 +1768,6 @@ class PowerOutletTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -1781,9 +1800,12 @@ class PowerOutletTestCase(TestCase): PowerPort.objects.bulk_create(power_ports) power_outlets = ( - PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), - PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), - PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), + PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, + description='First'), + PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, + description='Second'), + PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, + description='Third'), ) PowerOutlet.objects.bulk_create(power_outlets) @@ -1849,7 +1871,6 @@ class InterfaceTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -1876,12 +1897,21 @@ class InterfaceTestCase(TestCase): Device.objects.bulk_create(devices) interfaces = ( - Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), - Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), - Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), - Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, + mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', + description='First'), + Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, + mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', + description='Second'), + Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, + mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, + mac_address='00-00-00-00-00-03', description='Third'), + Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, + mgmt_only=True), + Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, + mgmt_only=True), + Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, + mgmt_only=False), ) Interface.objects.bulk_create(interfaces) @@ -1976,7 +2006,6 @@ class FrontPortTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -2013,12 +2042,18 @@ class FrontPortTestCase(TestCase): RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), - FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), - FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), - FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], + rear_port_position=1, description='First'), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, + rear_port=rear_ports[1], rear_port_position=2, description='Second'), + FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], + rear_port_position=3, description='Third'), + FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], + rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], + rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], + rear_port_position=1), ) FrontPort.objects.bulk_create(front_ports) @@ -2079,7 +2114,6 @@ class RearPortTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -2106,9 +2140,12 @@ class RearPortTestCase(TestCase): Device.objects.bulk_create(devices) rear_ports = ( - RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'), - RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'), - RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'), + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, + description='First'), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, + description='Second'), + RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, + description='Third'), RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4), RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5), RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6), @@ -2176,7 +2213,6 @@ class DeviceBayTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -2249,7 +2285,6 @@ class InventoryItemTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -2257,6 +2292,29 @@ class InventoryItemTestCase(TestCase): ) Manufacturer.objects.bulk_create(manufacturers) + 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(roles) + + types = ( + InventoryItemType( + model='Inventory Item Type 1', manufacturer=manufacturers[0], part_number='101', + slug='inventory-item-type=1' + ), + InventoryItemType( + model='Inventory Item Type 2', manufacturer=manufacturers[1], part_number='102', + slug='inventory-item-type=2' + ), + InventoryItemType( + model='Inventory Item Type 3', manufacturer=manufacturers[2], part_number='103', + slug='inventory-item-type=3' + ) + ) + InventoryItemType.objects.bulk_create(types) + device_type = DeviceType.objects.create(manufacturer=manufacturers[0], model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') @@ -2283,9 +2341,18 @@ class InventoryItemTestCase(TestCase): Device.objects.bulk_create(devices) inventory_items = ( - InventoryItem(device=devices[0], manufacturer=manufacturers[0], name='Inventory Item 1', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First'), - 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[0], site=sites[0], type=types[0], role=roles[0], name='Inventory Item 1', part_id='1001', + serial='ABC', asset_tag='1001', discovered=True, description='First' + ), + InventoryItem( + device=devices[1], site=sites[1], type=types[1], role=roles[1], name='Inventory Item 2', part_id='1002', + serial='DEF', asset_tag='1002', discovered=True, description='Second' + ), + InventoryItem( + device=devices[2], site=sites[2], type=types[2], role=roles[2], name='Inventory Item 3', part_id='1003', + serial='GHI', asset_tag='1003', discovered=False, description='Third' + ), ) InventoryItem.objects.bulk_create(inventory_items) @@ -2347,11 +2414,18 @@ class InventoryItemTestCase(TestCase): params = {'parent_id': [parent_items[0].pk, parent_items[1].pk]} 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]} + 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 = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + params = {'role': [roles[0].slug, roles[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_type(self): + types = InventoryItemType.objects.all()[:2] + params = {'type_id': [types[0].pk, types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'type': [types[0].slug, types[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_serial(self): @@ -2367,7 +2441,6 @@ class VirtualChassisTestCase(TestCase): @classmethod def setUpTestData(cls): - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') @@ -2438,7 +2511,6 @@ class CableTestCase(TestCase): @classmethod def setUpTestData(cls): - sites = ( Site(name='Site 1', slug='site-1'), Site(name='Site 2', slug='site-2'), @@ -2464,12 +2536,18 @@ class CableTestCase(TestCase): device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]), - Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2), - Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1), - Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], + position=1, tenant=tenants[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], + position=2, tenant=tenants[0]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], + position=1, tenant=tenants[1]), + Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], + position=2), + Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], + position=1), + Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], + position=2), ) Device.objects.bulk_create(devices) @@ -2490,12 +2568,24 @@ class CableTestCase(TestCase): Interface.objects.bulk_create(interfaces) # Cables - Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() - Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', + type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, + length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', + type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, + length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', + type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, + length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', + type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, + length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', + type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, + length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', + type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, + length_unit=CableLengthUnitChoices.UNIT_METER).save() def test_id(self): id_list = self.queryset.values_list('id', flat=True)[:2] @@ -2563,7 +2653,6 @@ class PowerPanelTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -2623,7 +2712,6 @@ class PowerFeedTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( Region(name='Region 1', slug='region-1'), Region(name='Region 2', slug='region-2'), @@ -2654,9 +2742,18 @@ class PowerFeedTestCase(TestCase): PowerPanel.objects.bulk_create(power_panels) power_feeds = ( - PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10), - PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20), - PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30), + PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', + status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, + supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, + amperage=100, max_utilization=10), + PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', + status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, + supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, + amperage=200, max_utilization=20), + PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', + status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, + supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, + amperage=300, max_utilization=30), ) PowerFeed.objects.bulk_create(power_feeds) @@ -2718,4 +2815,34 @@ class PowerFeedTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) +class InventoryItemRoleTestCase(TestCase): + queryset = InventoryItemRole.objects.all() + filterset = InventoryItemRoleFilterSet + + @classmethod + def setUpTestData(cls): + 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) + + def test_id(self): + id_list = self.queryset.values_list('id', flat=True)[:2] + params = {'id': [str(id) for id in id_list]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['Inventory Item Role 1', 'Inventory Item Role 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['inventory-item-role-1', 'inventory-item-role-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug1(self): + params = {'slug': ['inventory-item-role-1', 'test-role']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + # TODO: Connection filters diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index dbdb1526e..2309afac3 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1384,7 +1384,6 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.form_data = { 'device': device.pk, - 'manufacturer': manufacturer.pk, 'name': 'Inventory Item X', 'parent': None, 'discovered': False, @@ -1398,7 +1397,6 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, 'name_pattern': 'Inventory Item [4-6]', - 'manufacturer': manufacturer.pk, 'parent': None, 'discovered': False, 'part_id': '123456', @@ -1409,7 +1407,6 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_edit_data = { 'device': device.pk, - 'manufacturer': manufacturer.pk, 'part_id': '123456', 'description': 'New description', } diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c62800386..fd8995ed5 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -6,7 +6,7 @@ from . import views from .models import ( Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, - VirtualChassis, + VirtualChassis, InventoryItemRole, InventoryItemType, ) app_name = 'dcim' @@ -303,6 +303,30 @@ urlpatterns = [ path('inventory-items//edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), path('inventory-items//delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), + # Inventory item roles + path('inventory-item-roles/', views.InventoryItemRoleListView.as_view(), name='inventoryitemrole_list'), + path('inventory-item-roles/import/', views.InventoryItemRoleBulkImportView.as_view(), + name='inventoryitemrole_import'), + path('inventory-item-roles/add/', views.InventoryItemRoleCreateView.as_view(), name='inventoryitemrole_add'), + path('inventory-item-roles/delete/', views.InventoryItemRoleBulkDeleteView.as_view(), + name='inventoryitemrole_bulk_delete'), + path('inventory-item-roles//edit/', views.InventoryItemRoleEditView.as_view(), + name='inventoryitemrole_edit'), + path('inventory-item-roles//changelog/', ObjectChangeLogView.as_view(), + name='inventoryitemrole_changelog', + kwargs={'model': InventoryItemRole}), + + # Inventory Item types + path('inventory-item-types/', views.InventoryItemTypeListView.as_view(), name='inventoryitemtype_list'), + path('inventory-item-types/add/', views.InventoryItemTypeCreateView.as_view(), name='inventoryitemtype_add'), + path('inventory-item-types/import/', views.InventoryItemTypeImportView.as_view(), name='inventoryitemtype_import'), + path('inventory-item-types/edit/', views.InventoryItemTypeBulkEditView.as_view(), name='inventoryitemtype_bulk_edit'), + path('inventory-item-types/delete/', views.InventoryItemTypeBulkDeleteView.as_view(), name='inventoryitemtype_bulk_delete'), + path('inventory-item-types//', views.InventoryItemTypeView.as_view(), name='inventoryitemtype'), + path('inventory-item-types//edit/', views.InventoryItemTypeEditView.as_view(), name='inventoryitemtype_edit'), + path('inventory-item-types//delete/', views.InventoryItemTypeDeleteView.as_view(), name='inventoryitemtype_delete'), + path('inventory-item-types//changelog/', ObjectChangeLogView.as_view(), name='inventoryitemtype_changelog', kwargs={'model': InventoryItemType}), + # Cables path('cables/', views.CableListView.as_view(), name='cable_list'), path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 2bfd6df98..2e088ccb0 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -35,7 +35,7 @@ from .constants import NONCONNECTABLE_IFACE_TYPES from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + InventoryItem, InventoryItemRole, InventoryItemType, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) @@ -552,7 +552,7 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_manufacturer' queryset = Manufacturer.objects.annotate( devicetype_count=Count('device_types', distinct=True), - inventoryitem_count=Count('inventory_items', distinct=True), + inventoryitem_count=Count('inventory_item_types__instances', distinct=True), platform_count=Count('platforms', distinct=True), ) table = tables.ManufacturerTable @@ -1007,11 +1007,11 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): queryset = DeviceBayTemplate.objects.all() table = tables.DeviceBayTemplateTable - # # Device roles # + class DeviceRoleListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_devicerole' queryset = DeviceRole.objects.all() @@ -1180,7 +1180,7 @@ class DeviceInventoryView(PermissionRequiredMixin, View): inventory_items = InventoryItem.objects.filter( device=device, parent=None ).prefetch_related( - 'manufacturer', 'child_items' + 'type__manufacturer', 'child_items' ) return render(request, 'dcim/device_inventory.html', { @@ -2264,14 +2264,50 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): return '\n'.join(csv_data) +# +# Inventory item roles +# + + +class InventoryItemRoleListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_inventoryitemrole' + queryset = InventoryItemRole.objects.all() + table = tables.InventoryItemRoleTable + + +class InventoryItemRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_inventoryitemrole' + model = InventoryItemRole + model_form = forms.InventoryItemRoleForm + default_return_url = 'dcim:inventoryitemrole_list' + + +class InventoryItemRoleBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_inventoryitemrole' + model_form = forms.InventoryItemRoleCSVForm + table = tables.InventoryItemRoleTable + default_return_url = 'dcim:inventoryitemrole_list' + + +class InventoryItemRoleEditView(InventoryItemRoleCreateView): + permission_required = 'dcim.change_inventoryitemrole' + + +class InventoryItemRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_inventoryitemrole' + queryset = InventoryItemRole.objects.all() + filterset = filters.InventoryItemRoleFilterSet + table = tables.InventoryItemRoleTable + default_return_url = 'dcim:inventoryitemrole_list' # # Inventory items # + class InventoryItemListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_inventoryitem' - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') + queryset = InventoryItem.objects.prefetch_related('device', 'role', 'type__manufacturer') filterset = filters.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable @@ -2306,7 +2342,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView): class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_inventoryitem' - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') + queryset = InventoryItem.objects.prefetch_related('device', 'type__manufacturer') filterset = filters.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm @@ -2315,11 +2351,78 @@ class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView): class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_inventoryitem' - queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') + queryset = InventoryItem.objects.prefetch_related('device', 'type__manufacturer') table = tables.InventoryItemTable template_name = 'dcim/inventoryitem_bulk_delete.html' default_return_url = 'dcim:inventoryitem_list' +# +# Inventory Item types +# + + +class InventoryItemTypeListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_inventoryitemtype' + queryset = InventoryItemType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) + filterset = filters.InventoryItemTypeFilterSet + filterset_form = forms.InventoryItemTypeFilterForm + table = tables.InventoryItemTypeTable + + +class InventoryItemTypeView(PermissionRequiredMixin, View): + permission_required = 'dcim.view_inventoryitemtype' + + def get(self, request, pk): + + inventoryitemtype = get_object_or_404(InventoryItemType, pk=pk) + return render(request, 'dcim/inventoryitemtype.html', { + 'inventoryitemtype': inventoryitemtype, + }) + + +class InventoryItemTypeCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_inventoryitemtype' + model = InventoryItemType + model_form = forms.InventoryItemTypeForm + template_name = 'dcim/inventoryitemtype_edit.html' + default_return_url = 'dcim:inventoryitemtype_list' + + +class InventoryItemTypeEditView(InventoryItemTypeCreateView): + permission_required = 'dcim.change_inventoryitemtype' + + +class InventoryItemTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_inventoryitemtype' + model = InventoryItemType + default_return_url = 'dcim:inventoryitemtype_list' + + +class InventoryItemTypeImportView(PermissionRequiredMixin, ObjectImportView): + permission_required = [ + 'dcim.add_inventoryitemtype', + ] + model = InventoryItemType + model_form = forms.InventoryItemTypeImportForm + default_return_url = 'dcim:inventoryitemtype_import' + + +class InventoryItemTypeBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_inventoryitemtype' + queryset = InventoryItemType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) + filterset = filters.InventoryItemTypeFilterSet + table = tables.InventoryItemTypeTable + form = forms.InventoryItemTypeBulkEditForm + default_return_url = 'dcim:inventoryitemtype_list' + + +class InventoryItemTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_inventoryitemtype' + queryset = InventoryItemType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) + filterset = filters.InventoryItemTypeFilterSet + table = tables.InventoryItemTypeTable + default_return_url = 'dcim:inventoryitemtype_list' + # # Virtual chassis diff --git a/netbox/templates/dcim/inc/inventoryitem.html b/netbox/templates/dcim/inc/inventoryitem.html index 56ccfeace..2735f8a11 100644 --- a/netbox/templates/dcim/inc/inventoryitem.html +++ b/netbox/templates/dcim/inc/inventoryitem.html @@ -1,7 +1,7 @@ {{ item }} {% if not item.discovered %}{% endif %} - {{ item.manufacturer|default:"" }} + {{ item.type.manufacturer|default:"" }} {{ item.part_id }} {{ item.serial }} {{ item.asset_tag|default:"" }} diff --git a/netbox/templates/dcim/inventoryitemtype.html b/netbox/templates/dcim/inventoryitemtype.html new file mode 100644 index 000000000..4fb6efce4 --- /dev/null +++ b/netbox/templates/dcim/inventoryitemtype.html @@ -0,0 +1,89 @@ +{% extends '_base.html' %} +{% load buttons %} +{% load custom_links %} +{% load helpers %} + +{% block title %}{{ inventoryitemtype.manufacturer }} {{ inventoryitemtype.model }}{% endblock %} + +{% block header %} +
+
+ +
+
+
+ {% if perms.dcim.add_inventoryitemtype %} + {% clone_button inventoryitemtype %} + {% endif %} + {% if perms.dcim.change_inventoryitemtype %} + {% edit_button inventoryitemtype use_pk=True %} + {% endif %} + {% if perms.dcim.delete_inventoryitemtype %} + {% delete_button inventoryitemtype use_pk=True %} + {% endif %} +
+

{{ inventoryitemtype.manufacturer }} {{ inventoryitemtype.model }}

+ {% include 'inc/created_updated.html' with obj=inventoryitemtype %} +
+ {% custom_links inventoryitemtype %} +
+ +{% endblock %} + +{% block content %} +
+
+
+
+ Chassis +
+ + + + + + + + + + + + + +
Manufacturer{{ inventoryitemtype.manufacturer }}
Model Name + {{ inventoryitemtype.model }}
+ {{ inventoryitemtype.slug }} +
Part Number{{ inventoryitemtype.part_number|placeholder }}
+
+
+
+ {% include 'inc/custom_fields_panel.html' with obj=inventoryitemtype %} + {% include 'extras/inc/tags_panel.html' with tags=inventoryitemtype.tags.all url='dcim:inventoryitemtype_list' %} +
+
+ Comments +
+
+ {% if inventoryitemtype.comments %} + {{ inventoryitemtype.comments|gfm }} + {% else %} + None + {% endif %} +
+
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/inventoryitemtype_edit.html b/netbox/templates/dcim/inventoryitemtype_edit.html new file mode 100644 index 000000000..53a8b7f65 --- /dev/null +++ b/netbox/templates/dcim/inventoryitemtype_edit.html @@ -0,0 +1,22 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Inventory Item Type
+
+ {% render_field form.manufacturer %} + {% render_field form.model %} + {% render_field form.slug %} + {% render_field form.part_number %} +
+
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index e65d42623..1bd304f41 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -169,6 +169,25 @@ {% endif %} Inventory Items + + {% if perms.dcim.add_inventoryitemrole %} +
+ + +
+ {% endif %} + Inventory Item Roles + + + + {% if perms.dcim.add_inventoryitemtype %} +
+ + +
+ {% endif %} + Inventory Item Types +
  • diff --git a/scripts/cibuild.sh b/scripts/cibuild.sh index 282000b0a..ea173ad2e 100755 --- a/scripts/cibuild.sh +++ b/scripts/cibuild.sh @@ -27,7 +27,7 @@ fi # - E501: line greater than 80 characters in length pycodestyle \ --ignore=W504,E501 \ - netbox/ + netbox RC=$? if [[ $RC != 0 ]]; then echo -e "\n$(info) one or more PEP 8 errors detected, failing build."