1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Add Module model

This commit is contained in:
jeremystretch
2021-12-17 12:18:37 -05:00
parent e529d7fd3b
commit 7c60e3c0ff
27 changed files with 1542 additions and 89 deletions

View File

@ -22,6 +22,7 @@ __all__ = [
'NestedManufacturerSerializer',
'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer',
'NestedModuleTypeSerializer',
'NestedPlatformSerializer',
'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer',
@ -119,7 +120,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
#
# Device types
# Device/module types
#
class NestedManufacturerSerializer(WritableNestedSerializer):
@ -141,6 +142,20 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
fields = ['id', 'url', 'display', 'manufacturer', 'model']
#
# Component templates
#
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')

View File

@ -261,7 +261,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
#
# Device types
# Device/module types
#
class ManufacturerSerializer(PrimaryModelSerializer):
@ -294,6 +294,23 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
]
class ModuleTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = ModuleType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
#
# Component templates
#
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = NestedDeviceTypeSerializer()

View File

@ -16,9 +16,10 @@ router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet)
# Device types
# Device/module types
router.register('manufacturers', views.ManufacturerViewSet)
router.register('device-types', views.DeviceTypeViewSet)
router.register('module-types', views.ModuleTypeViewSet)
# Device type components
router.register('console-port-templates', views.ConsolePortTemplateViewSet)

View File

@ -271,7 +271,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
#
# Device types
# Device/module types
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
@ -283,6 +283,15 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(CustomFieldModelViewSet):
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
# module_count=count_related(Module, 'module_type')
)
serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet
brief_prefetch_fields = ['manufacturer']
#
# Device type components
#

View File

@ -43,6 +43,7 @@ __all__ = (
'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
'ModuleTypeFilterSet',
'PathEndpointFilterSet',
'PlatformFilterSet',
'PowerConnectionFilterSet',
@ -503,6 +504,83 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
return queryset.exclude(devicebaytemplates__isnull=value)
class ModuleTypeFilterSet(PrimaryModelFilterSet):
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)',
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label='Has console server ports',
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label='Has power ports',
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label='Has power outlets',
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label='Has interfaces',
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label='Has pass-through ports',
)
tag = TagFilter()
class Meta:
model = ModuleType
fields = ['id', 'model', '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(comments__icontains=value)
)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleporttemplates__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverporttemplates__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerporttemplates__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlettemplates__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfacetemplates__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontporttemplates__isnull=value,
rearporttemplates__isnull=value
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
@ -520,28 +598,36 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
return queryset.filter(name__icontains=value)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
moduletype_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleType.objects.all(),
field_name='module_type_id',
label='Module type (ID)',
)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = ConsolePortTemplate
fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type']
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
@ -552,7 +638,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompone
fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
@ -563,7 +649,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
@ -574,7 +660,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'color']
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None

View File

@ -34,6 +34,7 @@ __all__ = (
'ManufacturerBulkEditForm',
'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm',
'ModuleTypeBulkEditForm',
'PlatformBulkEditForm',
'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm',
@ -327,6 +328,9 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
queryset=Manufacturer.objects.all(),
required=False
)
part_number = forms.CharField(
required=False
)
u_height = forms.IntegerField(
min_value=1,
required=False
@ -343,7 +347,24 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
)
class Meta:
nullable_fields = ['airflow']
nullable_fields = ['part_number', 'airflow']
class ModuleTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleType.objects.all(),
widget=forms.MultipleHiddenInput()
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
part_number = forms.CharField(
required=False
)
class Meta:
nullable_fields = ['part_number']
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):

View File

@ -30,6 +30,7 @@ __all__ = (
'LocationFilterForm',
'ManufacturerFilterForm',
'ModuleBayFilterForm',
'ModuleTypeFilterForm',
'PlatformFilterForm',
'PowerConnectionFilterForm',
'PowerFeedFilterForm',
@ -337,7 +338,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
model = DeviceType
field_groups = [
['q', 'tag'],
['manufacturer_id', 'subdevice_role', 'airflow'],
['manufacturer_id', 'part_number', 'subdevice_role', 'airflow'],
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
]
manufacturer_id = DynamicModelMultipleChoiceField(
@ -346,6 +347,9 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
label=_('Manufacturer'),
fetch_trigger='open'
)
part_number = forms.CharField(
required=False
)
subdevice_role = forms.MultipleChoiceField(
choices=add_blank_choice(SubdeviceRoleChoices),
required=False,
@ -401,6 +405,67 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
class ModuleTypeFilterForm(CustomFieldModelFilterForm):
model = ModuleType
field_groups = [
['q', 'tag'],
['manufacturer_id', 'part_number'],
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
]
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
)
part_number = forms.CharField(
required=False
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
class DeviceRoleFilterForm(CustomFieldModelFilterForm):
model = DeviceRole
tag = TagFilterField(model)

View File

@ -41,6 +41,7 @@ __all__ = (
'ManufacturerForm',
'ModuleBayForm',
'ModuleBayTemplateForm',
'ModuleTypeForm',
'PlatformForm',
'PopulateDeviceBayForm',
'PowerFeedForm',
@ -414,6 +415,23 @@ class DeviceTypeForm(CustomFieldModelForm):
}
class ModuleTypeForm(CustomFieldModelForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all()
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ModuleType
fields = [
'manufacturer', 'model', 'part_number', 'comments', 'tags',
]
class DeviceRoleForm(CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(
@ -892,10 +910,11 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
}
@ -903,10 +922,11 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
}
@ -914,10 +934,11 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
}
@ -925,19 +946,21 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType
if hasattr(self.instance, 'device_type'):
# Limit power_port choices to current DeviceType/ModuleType
if self.instance.pk:
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=self.instance.device_type
device_type=self.instance.device_type,
module_type=self.instance.module_type
)
@ -945,10 +968,11 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
@ -957,20 +981,23 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'rear_port': StaticSelect(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit rear_port choices to current DeviceType
if hasattr(self.instance, 'device_type'):
# Limit rear_port choices to current DeviceType/ModuleType
if self.instance.pk:
self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
device_type=self.instance.device_type
device_type=self.instance.device_type,
module_type=self.instance.module_type
)
@ -978,10 +1005,11 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}

View File

@ -152,11 +152,20 @@ class ComponentTemplateCreateForm(ComponentForm):
queryset=Manufacturer.objects.all(),
required=False,
initial_params={
'device_types': 'device_type'
'device_types': 'device_type',
'module_types': 'module_type',
}
)
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer'
}
)
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer'
}
@ -171,7 +180,9 @@ class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect()
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
)
class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
@ -179,7 +190,9 @@ class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect()
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
)
class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
@ -198,8 +211,8 @@ class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text="Allocated power draw (watts)"
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw',
'description',
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw',
'allocated_draw', 'description',
)
@ -208,9 +221,13 @@ class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
choices=add_blank_choice(PowerOutletTypeChoices),
required=False
)
power_port = forms.ModelChoiceField(
power_port = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False
required=False,
query_params={
'devicetype_id': '$device_type',
'moduletype_id': '$module_type',
}
)
feed_leg = forms.ChoiceField(
choices=add_blank_choice(PowerOutletFeedLegChoices),
@ -218,21 +235,10 @@ class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
widget=StaticSelect()
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
'description',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType
device_type = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=device_type
)
class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField(
@ -243,7 +249,10 @@ class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
required=False,
label='Management only'
)
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description')
field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only',
'description',
)
class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
@ -260,7 +269,8 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text='Select one rear port assignment for each front port being created.',
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set',
'description',
)
def __init__(self, *args, **kwargs):
@ -325,7 +335,8 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text='The number of front ports which may be mapped to each rear port'
)
field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions',
'description',
)

View File

@ -12,6 +12,7 @@ __all__ = (
'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm',
'ModuleBayTemplateImportForm',
'ModuleTypeImportForm',
'PowerOutletTemplateImportForm',
'PowerPortTemplateImportForm',
'RearPortTemplateImportForm',
@ -32,6 +33,17 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
]
class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleType
fields = ['manufacturer', 'model', 'part_number', 'comments']
#
# Component template import forms
#

View File

@ -62,6 +62,9 @@ class DCIMQuery(graphene.ObjectType):
module_bay_template = ObjectField(ModuleBayTemplateType)
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
module_type = ObjectField(ModuleTypeType)
module_type_list = ObjectListField(ModuleTypeType)
platform = ObjectField(PlatformType)
platform_list = ObjectListField(PlatformType)

View File

@ -29,6 +29,7 @@ __all__ = (
'ManufacturerType',
'ModuleBayType',
'ModuleBayTemplateType',
'ModuleTypeType',
'PlatformType',
'PowerFeedType',
'PowerOutletType',
@ -272,6 +273,14 @@ class ModuleBayTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.ModuleBayTemplateFilterSet
class ModuleTypeType(PrimaryObjectType):
class Meta:
model = models.ModuleType
fields = '__all__'
filterset_class = filtersets.ModuleTypeFilterSet
class PlatformType(OrganizationalObjectType):
class Meta:

View File

@ -14,6 +14,150 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterModelOptions(
name='consoleporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='consoleserverporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='frontporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='interfacetemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='poweroutlettemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='powerporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='rearporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterField(
model_name='consoleporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='frontporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='interfacetemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='powerporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='rearporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.devicetype'),
),
migrations.CreateModel(
name='ModuleType',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('model', models.CharField(max_length=100)),
('part_number', models.CharField(blank=True, max_length=50)),
('comments', models.TextField(blank=True)),
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='module_types', to='dcim.manufacturer')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('manufacturer', 'model'),
'unique_together': {('manufacturer', 'model')},
},
),
migrations.AddField(
model_name='consoleporttemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='frontporttemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='interfacetemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='powerporttemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.moduletype'),
),
migrations.AddField(
model_name='rearporttemplate',
name='module_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.moduletype'),
),
migrations.AlterUniqueTogether(
name='consoleporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='consoleserverporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='frontporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name'), ('rear_port', 'rear_port_position')},
),
migrations.AlterUniqueTogether(
name='interfacetemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='poweroutlettemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='powerporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='rearporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.CreateModel(
name='ModuleBayTemplate',
fields=[

View File

@ -29,6 +29,7 @@ __all__ = (
'Manufacturer',
'ModuleBay',
'ModuleBayTemplate',
'ModuleType',
'Platform',
'PowerFeed',
'PowerOutlet',

View File

@ -64,7 +64,7 @@ class ComponentTemplateModel(ChangeLoggedModel):
"""
raise NotImplementedError()
def to_objectchange(self, action):
def to_objectchange(self, action, related_object=None):
# Annotate the parent DeviceType
try:
device_type = self.device_type
@ -74,8 +74,58 @@ class ComponentTemplateModel(ChangeLoggedModel):
return super().to_objectchange(action, related_object=device_type)
class ModularComponentTemplateModel(ComponentTemplateModel):
"""
A ComponentTemplateModel which supports optional assignment to a ModuleType.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='%(class)ss',
blank=True,
null=True
)
module_type = models.ForeignKey(
to='dcim.ModuleType',
on_delete=models.CASCADE,
related_name='%(class)ss',
blank=True,
null=True
)
class Meta:
abstract = True
def to_objectchange(self, action, related_object=None):
# Annotate the parent DeviceType or ModuleType
try:
if getattr(self, 'device_type'):
return super().to_objectchange(action, related_object=self.device_type)
except ObjectDoesNotExist:
pass
try:
if getattr(self, 'module_type'):
return super().to_objectchange(action, related_object=self.module_type)
except ObjectDoesNotExist:
pass
return super().to_objectchange(action)
def clean(self):
super().clean()
# A component template must belong to a DeviceType *or* to a ModuleType
if self.device_type and self.module_type:
raise ValidationError(
"A component template cannot be associated with both a device type and a module type."
)
if not self.device_type and not self.module_type:
raise ValidationError(
"A component template must be associated with either a device type or a module type."
)
@extras_features('webhooks')
class ConsolePortTemplate(ComponentTemplateModel):
class ConsolePortTemplate(ModularComponentTemplateModel):
"""
A template for a ConsolePort to be created for a new Device.
"""
@ -86,8 +136,11 @@ class ConsolePortTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device):
return ConsolePort(
@ -99,7 +152,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class ConsoleServerPortTemplate(ComponentTemplateModel):
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
"""
A template for a ConsoleServerPort to be created for a new Device.
"""
@ -110,8 +163,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device):
return ConsoleServerPort(
@ -123,7 +179,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class PowerPortTemplate(ComponentTemplateModel):
class PowerPortTemplate(ModularComponentTemplateModel):
"""
A template for a PowerPort to be created for a new Device.
"""
@ -146,8 +202,11 @@ class PowerPortTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device):
return PowerPort(
@ -170,7 +229,7 @@ class PowerPortTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class PowerOutletTemplate(ComponentTemplateModel):
class PowerOutletTemplate(ModularComponentTemplateModel):
"""
A template for a PowerOutlet to be created for a new Device.
"""
@ -194,17 +253,25 @@ class PowerOutletTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def clean(self):
super().clean()
# Validate power port assignment
if self.power_port and self.power_port.device_type != self.device_type:
raise ValidationError(
"Parent power port ({}) must belong to the same device type".format(self.power_port)
)
if self.power_port:
if self.device_type and self.power_port.device_type != self.device_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same device type"
)
if self.module_type and self.power_port.module_type != self.module_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same module type"
)
def instantiate(self, device):
if self.power_port:
@ -222,7 +289,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class InterfaceTemplate(ComponentTemplateModel):
class InterfaceTemplate(ModularComponentTemplateModel):
"""
A template for a physical data interface on a new Device.
"""
@ -243,8 +310,11 @@ class InterfaceTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device):
return Interface(
@ -257,7 +327,7 @@ class InterfaceTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class FrontPortTemplate(ComponentTemplateModel):
class FrontPortTemplate(ModularComponentTemplateModel):
"""
Template for a pass-through port on the front of a new Device.
"""
@ -282,9 +352,10 @@ class FrontPortTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
('rear_port', 'rear_port_position'),
)
@ -327,7 +398,7 @@ class FrontPortTemplate(ComponentTemplateModel):
@extras_features('webhooks')
class RearPortTemplate(ComponentTemplateModel):
class RearPortTemplate(ModularComponentTemplateModel):
"""
Template for a pass-through port on the rear of a new Device.
"""
@ -347,8 +418,11 @@ class RearPortTemplate(ComponentTemplateModel):
)
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device):
return RearPort(

View File

@ -26,6 +26,7 @@ __all__ = (
'DeviceRole',
'DeviceType',
'Manufacturer',
'ModuleType',
'Platform',
'VirtualChassis',
)
@ -253,6 +254,15 @@ class DeviceType(PrimaryModel):
}
for c in self.rearporttemplates.all()
]
if self.modulebaytemplates.exists():
data['module-bays'] = [
{
'name': c.name,
'label': c.label,
'description': c.description,
}
for c in self.modulebaytemplates.all()
]
if self.devicebaytemplates.exists():
data['device-bays'] = [
{
@ -342,6 +352,136 @@ class DeviceType(PrimaryModel):
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ModuleType(PrimaryModel):
"""
A ModuleType represents a hardware element that can be installed within a device and which houses additional
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
DeviceType, each ModuleType can have console, power, interface, and pass-through port templates assigned to it. It
cannot, however house device bays or module bays.
"""
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
related_name='module_types'
)
model = models.CharField(
max_length=100
)
part_number = models.CharField(
max_length=50,
blank=True,
help_text='Discrete part number (optional)'
)
comments = models.TextField(
blank=True
)
clone_fields = ('manufacturer',)
class Meta:
ordering = ('manufacturer', 'model')
unique_together = (
('manufacturer', 'model'),
)
def __str__(self):
return self.model
def get_absolute_url(self):
return reverse('dcim:moduletype', args=[self.pk])
def to_yaml(self):
data = OrderedDict((
('manufacturer', self.manufacturer.name),
('model', self.model),
('part_number', self.part_number),
('comments', self.comments),
))
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
{
'name': c.name,
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
}
for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
{
'name': c.name,
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
}
for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
{
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
}
for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
{
'name': c.name,
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
}
for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
{
'name': c.name,
'type': c.type,
'positions': c.positions,
'label': c.label,
'description': c.description,
}
for c in self.rearporttemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)
#
# Devices
#

View File

@ -6,6 +6,7 @@ from dcim.models import ConsolePort, Interface, PowerPort
from .cables import *
from .devices import *
from .devicetypes import *
from .moduletypes import *
from .power import *
from .racks import *
from .sites import *

View File

@ -0,0 +1,34 @@
import django_tables2 as tables
from dcim.models import ModuleType
from utilities.tables import BaseTable, MarkdownColumn, TagColumn, ToggleColumn
__all__ = (
'ModuleTypeTable',
)
class ModuleTypeTable(BaseTable):
pk = ToggleColumn()
model = tables.Column(
linkify=True,
verbose_name='Device Type'
)
# instance_count = LinkedCountColumn(
# viewname='dcim:module_list',
# url_params={'module_type_id': 'pk'},
# verbose_name='Instances'
# )
comments = MarkdownColumn()
tags = TagColumn(
url_name='dcim:moduletype_list'
)
class Meta(BaseTable.Meta):
model = ModuleType
fields = (
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number',
)

View File

@ -470,6 +470,45 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
]
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
model = ModuleType
brief_fields = ['display', 'id', 'manufacturer', 'model', 'url']
bulk_update_data = {
'part_number': 'ABC123',
}
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
)
Manufacturer.objects.bulk_create(manufacturers)
module_types = (
ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
ModuleType(manufacturer=manufacturers[0], model='Module Type 2'),
ModuleType(manufacturer=manufacturers[0], model='Module Type 3'),
)
ModuleType.objects.bulk_create(module_types)
cls.create_data = [
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 4',
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 5',
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 6',
},
]
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsolePortTemplate
brief_fields = ['display', 'id', 'name', 'url']

View File

@ -773,6 +773,110 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all()
filterset = ModuleTypeFilterSet
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
module_types = (
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1'),
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2'),
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3'),
)
ModuleType.objects.bulk_create(module_types)
# Add component templates for filtering
ConsolePortTemplate.objects.bulk_create((
ConsolePortTemplate(module_type=module_types[0], name='Console Port 1'),
ConsolePortTemplate(module_type=module_types[1], name='Console Port 2'),
))
ConsoleServerPortTemplate.objects.bulk_create((
ConsoleServerPortTemplate(module_type=module_types[0], name='Console Server Port 1'),
ConsoleServerPortTemplate(module_type=module_types[1], name='Console Server Port 2'),
))
PowerPortTemplate.objects.bulk_create((
PowerPortTemplate(module_type=module_types[0], name='Power Port 1'),
PowerPortTemplate(module_type=module_types[1], name='Power Port 2'),
))
PowerOutletTemplate.objects.bulk_create((
PowerOutletTemplate(module_type=module_types[0], name='Power Outlet 1'),
PowerOutletTemplate(module_type=module_types[1], name='Power Outlet 2'),
))
InterfaceTemplate.objects.bulk_create((
InterfaceTemplate(module_type=module_types[0], name='Interface 1'),
InterfaceTemplate(module_type=module_types[1], name='Interface 2'),
))
rear_ports = (
RearPortTemplate(module_type=module_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create((
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
def test_model(self):
params = {'model': ['Model 1', 'Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_part_number(self):
params = {'part_number': ['Part Number 1', 'Part Number 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_console_ports(self):
params = {'console_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'console_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_console_server_ports(self):
params = {'console_server_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'console_server_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_power_ports(self):
params = {'power_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'power_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_power_outlets(self):
params = {'power_outlets': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'power_outlets': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_interfaces(self):
params = {'interfaces': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'interfaces': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_pass_through_ports(self):
params = {'pass_through_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'pass_through_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()
filterset = ConsolePortTemplateFilterSet

View File

@ -591,7 +591,7 @@ model: TEST-1000
slug: test-1000
u_height: 2
subdevice_role: parent
comments: test comment
comments: Test comment
console-ports:
- name: Console Port 1
type: de-9
@ -686,53 +686,53 @@ device-bays:
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
dt = DeviceType.objects.get(model='TEST-1000')
self.assertEqual(dt.comments, 'test comment')
device_type = DeviceType.objects.get(model='TEST-1000')
self.assertEqual(device_type.comments, 'Test comment')
# Verify all of the components were created
self.assertEqual(dt.consoleporttemplates.count(), 3)
self.assertEqual(device_type.consoleporttemplates.count(), 3)
cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1')
self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
self.assertEqual(dt.consoleserverporttemplates.count(), 3)
self.assertEqual(device_type.consoleserverporttemplates.count(), 3)
csp1 = ConsoleServerPortTemplate.objects.first()
self.assertEqual(csp1.name, 'Console Server Port 1')
self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
self.assertEqual(dt.powerporttemplates.count(), 3)
self.assertEqual(device_type.powerporttemplates.count(), 3)
pp1 = PowerPortTemplate.objects.first()
self.assertEqual(pp1.name, 'Power Port 1')
self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
self.assertEqual(dt.poweroutlettemplates.count(), 3)
self.assertEqual(device_type.poweroutlettemplates.count(), 3)
po1 = PowerOutletTemplate.objects.first()
self.assertEqual(po1.name, 'Power Outlet 1')
self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(dt.interfacetemplates.count(), 3)
self.assertEqual(device_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
self.assertTrue(iface1.mgmt_only)
self.assertEqual(dt.rearporttemplates.count(), 3)
self.assertEqual(device_type.rearporttemplates.count(), 3)
rp1 = RearPortTemplate.objects.first()
self.assertEqual(rp1.name, 'Rear Port 1')
self.assertEqual(dt.frontporttemplates.count(), 3)
self.assertEqual(device_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(dt.modulebaytemplates.count(), 3)
self.assertEqual(device_type.modulebaytemplates.count(), 3)
db1 = ModuleBayTemplate.objects.first()
self.assertEqual(db1.name, 'Module Bay 1')
self.assertEqual(dt.devicebaytemplates.count(), 3)
self.assertEqual(device_type.devicebaytemplates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1')
@ -741,7 +741,7 @@ device-bays:
self.add_permissions('dcim.view_devicetype')
# Test default YAML export
response = self.client.get('{}?export'.format(url))
response = self.client.get(f'{url}?export')
self.assertEqual(response.status_code, 200)
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
self.assertEqual(len(data), 3)
@ -754,6 +754,300 @@ device-bays:
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
# TODO: Change base class to PrimaryObjectViewTestCase
# Blocked by absence of bulk import view for ModuleTypes
class ModuleTypeTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.GetObjectChangelogViewTestCase,
ViewTestCases.CreateObjectViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
model = ModuleType
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2')
)
Manufacturer.objects.bulk_create(manufacturers)
ModuleType.objects.bulk_create([
ModuleType(model='Module Type 1', manufacturer=manufacturers[0]),
ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'manufacturer': manufacturers[1].pk,
'model': 'Device Type X',
'part_number': '123ABC',
'comments': 'Some comments',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'manufacturer': manufacturers[1].pk,
'part_number': '456DEF',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_consoleports(self):
moduletype = ModuleType.objects.first()
console_ports = (
ConsolePortTemplate(module_type=moduletype, name='Console Port 1'),
ConsolePortTemplate(module_type=moduletype, name='Console Port 2'),
ConsolePortTemplate(module_type=moduletype, name='Console Port 3'),
)
ConsolePortTemplate.objects.bulk_create(console_ports)
url = reverse('dcim:moduletype_consoleports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_consoleserverports(self):
moduletype = ModuleType.objects.first()
console_server_ports = (
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 1'),
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 2'),
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 3'),
)
ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)
url = reverse('dcim:moduletype_consoleserverports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_powerports(self):
moduletype = ModuleType.objects.first()
power_ports = (
PowerPortTemplate(module_type=moduletype, name='Power Port 1'),
PowerPortTemplate(module_type=moduletype, name='Power Port 2'),
PowerPortTemplate(module_type=moduletype, name='Power Port 3'),
)
PowerPortTemplate.objects.bulk_create(power_ports)
url = reverse('dcim:moduletype_powerports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_poweroutlets(self):
moduletype = ModuleType.objects.first()
power_outlets = (
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 1'),
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 2'),
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 3'),
)
PowerOutletTemplate.objects.bulk_create(power_outlets)
url = reverse('dcim:moduletype_poweroutlets', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_interfaces(self):
moduletype = ModuleType.objects.first()
interfaces = (
InterfaceTemplate(module_type=moduletype, name='Interface 1'),
InterfaceTemplate(module_type=moduletype, name='Interface 2'),
InterfaceTemplate(module_type=moduletype, name='Interface 3'),
)
InterfaceTemplate.objects.bulk_create(interfaces)
url = reverse('dcim:moduletype_interfaces', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_rearports(self):
moduletype = ModuleType.objects.first()
rear_ports = (
RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
)
RearPortTemplate.objects.bulk_create(rear_ports)
url = reverse('dcim:moduletype_rearports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_frontports(self):
moduletype = ModuleType.objects.first()
rear_ports = (
RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
)
RearPortTemplate.objects.bulk_create(rear_ports)
front_ports = (
FrontPortTemplate(module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
FrontPortTemplate(module_type=moduletype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
FrontPortTemplate(module_type=moduletype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
)
FrontPortTemplate.objects.bulk_create(front_ports)
url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_objects(self):
"""
Custom import test for YAML-based imports (versus CSV)
"""
IMPORT_DATA = """
manufacturer: Generic
model: TEST-1000
comments: Test comment
console-ports:
- name: Console Port 1
type: de-9
- name: Console Port 2
type: de-9
- name: Console Port 3
type: de-9
console-server-ports:
- name: Console Server Port 1
type: rj-45
- name: Console Server Port 2
type: rj-45
- name: Console Server Port 3
type: rj-45
power-ports:
- name: Power Port 1
type: iec-60320-c14
- name: Power Port 2
type: iec-60320-c14
- name: Power Port 3
type: iec-60320-c14
power-outlets:
- name: Power Outlet 1
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
- name: Power Outlet 2
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
- name: Power Outlet 3
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
interfaces:
- name: Interface 1
type: 1000base-t
mgmt_only: true
- name: Interface 2
type: 1000base-t
- name: Interface 3
type: 1000base-t
rear-ports:
- name: Rear Port 1
type: 8p8c
- name: Rear Port 2
type: 8p8c
- name: Rear Port 3
type: 8p8c
front-ports:
- name: Front Port 1
type: 8p8c
rear_port: Rear Port 1
- name: Front Port 2
type: 8p8c
rear_port: Rear Port 2
- name: Front Port 3
type: 8p8c
rear_port: Rear Port 3
"""
# Create the manufacturer
Manufacturer(name='Generic', slug='generic').save()
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_moduletype',
'dcim.add_moduletype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
)
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:moduletype_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
module_type = ModuleType.objects.get(model='TEST-1000')
self.assertEqual(module_type.comments, 'Test comment')
# Verify all the components were created
self.assertEqual(module_type.consoleporttemplates.count(), 3)
cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1')
self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
self.assertEqual(module_type.consoleserverporttemplates.count(), 3)
csp1 = ConsoleServerPortTemplate.objects.first()
self.assertEqual(csp1.name, 'Console Server Port 1')
self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
self.assertEqual(module_type.powerporttemplates.count(), 3)
pp1 = PowerPortTemplate.objects.first()
self.assertEqual(pp1.name, 'Power Port 1')
self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
self.assertEqual(module_type.poweroutlettemplates.count(), 3)
po1 = PowerOutletTemplate.objects.first()
self.assertEqual(po1.name, 'Power Outlet 1')
self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(module_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
self.assertTrue(iface1.mgmt_only)
self.assertEqual(module_type.rearporttemplates.count(), 3)
rp1 = RearPortTemplate.objects.first()
self.assertEqual(rp1.name, 'Rear Port 1')
self.assertEqual(module_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
def test_export_objects(self):
url = reverse('dcim:moduletype_list')
self.add_permissions('dcim.view_moduletype')
# Test default YAML export
response = self.client.get(f'{url}?export')
self.assertEqual(response.status_code, 200)
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
self.assertEqual(len(data), 3)
self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
self.assertEqual(data[0]['model'], 'Module Type 1')
# Test table-based export
response = self.client.get(f'{url}?export=table')
self.assertHttpStatus(response, 200)
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
#
# DeviceType components
#

View File

@ -120,6 +120,25 @@ urlpatterns = [
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
# Module types
path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'),
path('module-types/add/', views.ModuleTypeEditView.as_view(), name='moduletype_add'),
path('module-types/import/', views.ModuleTypeImportView.as_view(), name='moduletype_import'),
path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'),
path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'),
path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'),
path('module-types/<int:pk>/console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
path('module-types/<int:pk>/console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
path('module-types/<int:pk>/power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
path('module-types/<int:pk>/power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
path('module-types/<int:pk>/interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
path('module-types/<int:pk>/front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
path('module-types/<int:pk>/rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
path('module-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}),
path('module-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
# Console port templates
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),

View File

@ -30,9 +30,9 @@ from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import (
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, ModuleBay, ModuleBayTemplate, PathEndpoint, Platform, PowerFeed, PowerOutlet,
PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort,
RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
InventoryItem, Manufacturer, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
)
@ -56,6 +56,14 @@ class DeviceTypeComponentsView(DeviceComponentsView):
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
class ModuleTypeComponentsView(DeviceComponentsView):
queryset = ModuleType.objects.all()
template_name = 'dcim/moduletype/component_templates.html'
def get_children(self, request, parent):
return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent)
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
"""
An extendable view for disconnection console/power/interface components in bulk.
@ -902,6 +910,123 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceTypeTable
#
# Module types
#
class ModuleTypeListView(generic.ObjectListView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
filterset_form = forms.ModuleTypeFilterForm
table = tables.ModuleTypeTable
class ModuleTypeView(generic.ObjectView):
queryset = ModuleType.objects.prefetch_related('manufacturer')
def get_extra_context(self, request, instance):
# instance_count = Module.objects.restrict(request.user).filter(device_type=instance).count()
return {
# 'instance_count': instance_count,
'active_tab': 'moduletype',
}
class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet
class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet
class ModuleTypePowerPortsView(ModuleTypeComponentsView):
child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet
class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet
class ModuleTypeInterfacesView(ModuleTypeComponentsView):
child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet
class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet
class ModuleTypeRearPortsView(ModuleTypeComponentsView):
child_model = RearPortTemplate
table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet
class ModuleTypeEditView(generic.ObjectEditView):
queryset = ModuleType.objects.all()
model_form = forms.ModuleTypeForm
class ModuleTypeDeleteView(generic.ObjectDeleteView):
queryset = ModuleType.objects.all()
class ModuleTypeImportView(generic.ObjectImportView):
additional_permissions = [
'dcim.add_moduletype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
]
queryset = ModuleType.objects.all()
model_form = forms.ModuleTypeImportForm
related_object_forms = OrderedDict((
('console-ports', forms.ConsolePortTemplateImportForm),
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
('power-ports', forms.PowerPortTemplateImportForm),
('power-outlets', forms.PowerOutletTemplateImportForm),
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
))
class ModuleTypeBulkEditView(generic.BulkEditView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
form = forms.ModuleTypeBulkEditForm
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
#
# Console port templates
#

View File

@ -148,6 +148,7 @@ DEVICES_MENU = Menu(
label='Device Types',
items=(
get_model_item('dcim', 'devicetype', 'Device Types'),
get_model_item('dcim', 'moduletype', 'Module Types'),
get_model_item('dcim', 'manufacturer', 'Manufacturers'),
),
),

View File

@ -0,0 +1,48 @@
{% extends 'dcim/moduletype/base.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Module Type</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>Manufacturer</td>
<td><a href="{{ object.manufacturer.get_absolute_url }}">{{ object.manufacturer }}</a></td>
</tr>
<tr>
<td>Model Name</td>
<td>{{ object.model }}</td>
</tr>
<tr>
<td>Part Number</td>
<td>{{ object.part_number|placeholder }}</td>
</tr>
{% comment %}
<tr>
<td>Instances</td>
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
</tr>
{% endcomment %}
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,108 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>
{% endblock %}
{% block extra_controls %}
{% if perms.dcim.change_devicetype %}
<div class="dropdown">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
{% endif %}
{% if perms.dcim.add_powerporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
{% endif %}
{% if perms.dcim.add_interfacetemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
{% endif %}
{% if perms.dcim.add_frontporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
{% endif %}
{% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block tab_items %}
<li role="presentation" class="nav-item">
<a href="{% url 'dcim:devicetype' pk=object.pk %}" class="nav-link{% if active_tab == 'moduletype' %} active{% endif %}">
Module Type
</a>
</li>
{% with interface_count=object.interfacetemplates.count %}
{% if interface_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'interface-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with frontport_count=object.frontporttemplates.count %}
{% if frontport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'front-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with rearport_count=object.rearporttemplates.count %}
{% if rearport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'rear-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleport_count=object.consoleporttemplates.count %}
{% if consoleport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleserverport_count=object.consoleserverporttemplates.count %}
{% if consoleserverport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-server-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with powerport_count=object.powerporttemplates.count %}
{% if powerport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with poweroutlet_count=object.poweroutlettemplates.count %}
{% if poweroutlet_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-outlet-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
</li>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'dcim/moduletype/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% block content %}
{% if perms.dcim.change_moduletype %}
<form method="post">
{% csrf_token %}
<div class="card">
<h5 class="card-header">{{ title }}</h5>
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
<div class="card-footer noprint">
{% if table.rows %}
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> Rename
</button>
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</button>
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
<div class="float-end">
<a href="{% url table.Meta.model|viewname:"add" %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add {{ title }}
</a>
</div>
<div class="clearfix"></div>
</div>
</div>
</form>
{% else %}
<div class="card">
<h5 class="card-header">{{ title }}</h5>
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
{% endif %}
{% endblock content %}