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:
@ -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')
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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=[
|
||||
|
@ -29,6 +29,7 @@ __all__ = (
|
||||
'Manufacturer',
|
||||
'ModuleBay',
|
||||
'ModuleBayTemplate',
|
||||
'ModuleType',
|
||||
'Platform',
|
||||
'PowerFeed',
|
||||
'PowerOutlet',
|
||||
|
@ -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,16 +253,24 @@ 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:
|
||||
if self.power_port:
|
||||
if self.device_type 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)
|
||||
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):
|
||||
@ -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(
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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 *
|
||||
|
34
netbox/dcim/tables/moduletypes.py
Normal file
34
netbox/dcim/tables/moduletypes.py
Normal 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',
|
||||
)
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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'),
|
||||
),
|
||||
),
|
||||
|
48
netbox/templates/dcim/moduletype.html
Normal file
48
netbox/templates/dcim/moduletype.html
Normal 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 %}
|
108
netbox/templates/dcim/moduletype/base.html
Normal file
108
netbox/templates/dcim/moduletype/base.html
Normal 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 %}
|
44
netbox/templates/dcim/moduletype/component_templates.html
Normal file
44
netbox/templates/dcim/moduletype/component_templates.html
Normal 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 %}
|
Reference in New Issue
Block a user