Add ModuleTypeProfile & related fields

This commit is contained in:
Jeremy Stretch 2025-03-25 16:27:07 -04:00
parent 65ad972a1c
commit 678e6f1bd5
25 changed files with 412 additions and 20 deletions

View File

@ -82,6 +82,10 @@ gunicorn
# https://jinja.palletsprojects.com/changes/ # https://jinja.palletsprojects.com/changes/
Jinja2 Jinja2
# JSON schema validation
# https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst
jsonschema
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://python-markdown.github.io/changelog/ # https://python-markdown.github.io/changelog/
Markdown Markdown

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from rest_framework import serializers from rest_framework import serializers
from dcim.choices import * from dcim.choices import *
from dcim.models import DeviceType, ModuleType from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox.choices import * from netbox.choices import *
@ -13,6 +13,7 @@ from .platforms import PlatformSerializer
__all__ = ( __all__ = (
'DeviceTypeSerializer', 'DeviceTypeSerializer',
'ModuleTypeProfileSerializer',
'ModuleTypeSerializer', 'ModuleTypeSerializer',
) )
@ -62,7 +63,23 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
class ModuleTypeProfileSerializer(NetBoxModelSerializer):
class Meta:
model = ModuleTypeProfile
fields = [
'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ModuleTypeSerializer(NetBoxModelSerializer): class ModuleTypeSerializer(NetBoxModelSerializer):
profile = ModuleTypeProfileSerializer(
nested=True,
required=False,
allow_null=True
)
manufacturer = ManufacturerSerializer( manufacturer = ManufacturerSerializer(
nested=True nested=True
) )
@ -82,8 +99,8 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow', 'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields', 'weight', 'weight_unit', 'description', 'attributes', 'comments', 'tags', 'custom_fields', 'created',
'created', 'last_updated', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description')

View File

@ -21,6 +21,7 @@ router.register('rack-reservations', views.RackReservationViewSet)
router.register('manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register('device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
router.register('module-types', views.ModuleTypeViewSet) router.register('module-types', views.ModuleTypeViewSet)
router.register('module-type-profiles', views.ModuleTypeProfileViewSet)
# Device type components # Device type components
router.register('console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)

View File

@ -269,6 +269,12 @@ class DeviceTypeViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceTypeFilterSet filterset_class = filtersets.DeviceTypeFilterSet
class ModuleTypeProfileViewSet(NetBoxModelViewSet):
queryset = ModuleTypeProfile.objects.all()
serializer_class = serializers.ModuleTypeProfileSerializer
filterset_class = filtersets.ModuleTypeProfileFilterSet
class ModuleTypeViewSet(NetBoxModelViewSet): class ModuleTypeViewSet(NetBoxModelViewSet):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
serializer_class = serializers.ModuleTypeSerializer serializer_class = serializers.ModuleTypeSerializer

View File

@ -59,6 +59,7 @@ __all__ = (
'ModuleBayTemplateFilterSet', 'ModuleBayTemplateFilterSet',
'ModuleFilterSet', 'ModuleFilterSet',
'ModuleTypeFilterSet', 'ModuleTypeFilterSet',
'ModuleTypeProfileFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
'PowerConnectionFilterSet', 'PowerConnectionFilterSet',
@ -674,7 +675,33 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
return queryset.exclude(inventoryitemtemplates__isnull=value) return queryset.exclude(inventoryitemtemplates__isnull=value)
class ModuleTypeProfileFilterSet(NetBoxModelFilterSet):
class Meta:
model = ModuleTypeProfile
fields = ('id', 'name', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class ModuleTypeFilterSet(NetBoxModelFilterSet): class ModuleTypeFilterSet(NetBoxModelFilterSet):
profile_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleTypeProfile.objects.all(),
label=_('Profile (ID)'),
)
profile = django_filters.ModelMultipleChoiceFilter(
field_name='profile__name',
queryset=ModuleTypeProfile.objects.all(),
to_field_name='name',
label=_('Profile (name)'),
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
label=_('Manufacturer (ID)'), label=_('Manufacturer (ID)'),

View File

@ -46,6 +46,7 @@ __all__ = (
'ModuleBayBulkEditForm', 'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm', 'ModuleBayTemplateBulkEditForm',
'ModuleTypeBulkEditForm', 'ModuleTypeBulkEditForm',
'ModuleTypeProfileBulkEditForm',
'PlatformBulkEditForm', 'PlatformBulkEditForm',
'PowerFeedBulkEditForm', 'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm', 'PowerOutletBulkEditForm',
@ -574,7 +575,31 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments') nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm):
schema = forms.JSONField(
label=_('Schema'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField()
model = ModuleTypeProfile
fieldsets = (
FieldSet('name', 'description', 'schema', name=_('Profile')),
)
nullable_fields = ('description', 'comments')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
profile = DynamicModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -609,7 +634,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')), FieldSet('profile', 'manufacturer', 'part_number', 'description', name=_('Module Type')),
FieldSet( FieldSet(
'airflow', 'airflow',
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),

View File

@ -39,6 +39,7 @@ __all__ = (
'ModuleImportForm', 'ModuleImportForm',
'ModuleBayImportForm', 'ModuleBayImportForm',
'ModuleTypeImportForm', 'ModuleTypeImportForm',
'ModuleTypeProfileImportForm',
'PlatformImportForm', 'PlatformImportForm',
'PowerFeedImportForm', 'PowerFeedImportForm',
'PowerOutletImportForm', 'PowerOutletImportForm',
@ -427,7 +428,22 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
] ]
class ModuleTypeProfileImportForm(NetBoxModelImportForm):
class Meta:
model = ModuleTypeProfile
fields = [
'name', 'description', 'schema', 'comments', 'tags',
]
class ModuleTypeImportForm(NetBoxModelImportForm): class ModuleTypeImportForm(NetBoxModelImportForm):
profile = forms.ModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(),
to_field_name='name',
required=False
)
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),

View File

@ -39,6 +39,7 @@ __all__ = (
'ModuleFilterForm', 'ModuleFilterForm',
'ModuleBayFilterForm', 'ModuleBayFilterForm',
'ModuleTypeFilterForm', 'ModuleTypeFilterForm',
'ModuleTypeProfileFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
'PowerConnectionFilterForm', 'PowerConnectionFilterForm',
'PowerFeedFilterForm', 'PowerFeedFilterForm',
@ -602,11 +603,19 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
) )
class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm):
model = ModuleTypeProfile
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
)
selector_fields = ('filter_id', 'q')
class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')), FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
FieldSet( FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', name=_('Components') 'pass_through_ports', name=_('Components')
@ -614,6 +623,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
FieldSet('weight', 'weight_unit', name=_('Weight')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
selector_fields = ('filter_id', 'q', 'manufacturer_id') selector_fields = ('filter_id', 'q', 'manufacturer_id')
profile_id = DynamicModelMultipleChoiceField(
queryset=ModuleTypeProfile.objects.all(),
required=False,
label=_('Profile')
)
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,

View File

@ -48,6 +48,7 @@ __all__ = (
'ModuleBayForm', 'ModuleBayForm',
'ModuleBayTemplateForm', 'ModuleBayTemplateForm',
'ModuleTypeForm', 'ModuleTypeForm',
'ModuleTypeProfileForm',
'PlatformForm', 'PlatformForm',
'PopulateDeviceBayForm', 'PopulateDeviceBayForm',
'PowerFeedForm', 'PowerFeedForm',
@ -404,7 +405,26 @@ class DeviceTypeForm(NetBoxModelForm):
} }
class ModuleTypeProfileForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
)
class Meta:
model = ModuleTypeProfile
fields = [
'name', 'description', 'schema', 'comments', 'tags',
]
class ModuleTypeForm(NetBoxModelForm): class ModuleTypeForm(NetBoxModelForm):
profile = DynamicModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
queryset=Manufacturer.objects.all() queryset=Manufacturer.objects.all()
@ -412,15 +432,16 @@ class ModuleTypeForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), FieldSet('profile', 'manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis')) FieldSet('attributes', name=_('Profile Attributes')),
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
) )
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description', 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'comments', 'tags', 'attributes', 'comments', 'tags',
] ]

View File

@ -68,6 +68,7 @@ __all__ = (
'ModuleBayFilter', 'ModuleBayFilter',
'ModuleBayTemplateFilter', 'ModuleBayTemplateFilter',
'ModuleTypeFilter', 'ModuleTypeFilter',
'ModuleTypeProfileFilter',
'PlatformFilter', 'PlatformFilter',
'PowerFeedFilter', 'PowerFeedFilter',
'PowerOutletFilter', 'PowerOutletFilter',
@ -559,6 +560,11 @@ class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field() position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleType, lookups=True) @strawberry_django.filter(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (

View File

@ -77,6 +77,9 @@ class DCIMQuery:
module_bay_template: ModuleBayTemplateType = strawberry_django.field() module_bay_template: ModuleBayTemplateType = strawberry_django.field()
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field() module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
module_type_profile: ModuleTypeProfileType = strawberry_django.field()
module_type_profile_list: List[ModuleTypeProfileType] = strawberry_django.field()
module_type: ModuleTypeType = strawberry_django.field() module_type: ModuleTypeType = strawberry_django.field()
module_type_list: List[ModuleTypeType] = strawberry_django.field() module_type_list: List[ModuleTypeType] = strawberry_django.field()

View File

@ -61,6 +61,7 @@ __all__ = (
'ModuleType', 'ModuleType',
'ModuleBayType', 'ModuleBayType',
'ModuleBayTemplateType', 'ModuleBayTemplateType',
'ModuleTypeProfileType',
'ModuleTypeType', 'ModuleTypeType',
'PlatformType', 'PlatformType',
'PowerFeedType', 'PowerFeedType',
@ -593,6 +594,16 @@ class ModuleBayTemplateType(ModularComponentTemplateType):
pass pass
@strawberry_django.type(
models.ModuleTypeProfile,
fields='__all__',
filters=ModuleTypeProfileFilter,
pagination=True
)
class ModuleTypeProfileType(NetBoxObjectType):
moduletypes: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(
models.ModuleType, models.ModuleType,
fields='__all__', fields='__all__',
@ -600,6 +611,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType):
pagination=True pagination=True
) )
class ModuleTypeType(NetBoxObjectType): class ModuleTypeType(NetBoxObjectType):
profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]

View File

@ -0,0 +1,57 @@
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0202_location_comments_region_comments_sitegroup_comments'),
('extras', '0125_exporttemplate_file_name'),
]
operations = [
migrations.CreateModel(
name='ModuleTypeProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('name', models.CharField(max_length=100, unique=True)),
('schema', models.JSONField()),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'module type profile',
'verbose_name_plural': 'module type profiles',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='moduletype',
name='attributes',
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name='moduletype',
name='profile',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='module_types',
to='dcim.moduletypeprofile',
),
),
migrations.AlterModelOptions(
name='moduletype',
options={'ordering': ('profile', 'manufacturer', 'model')},
),
]

View File

@ -15,9 +15,34 @@ from .device_components import *
__all__ = ( __all__ = (
'Module', 'Module',
'ModuleType', 'ModuleType',
'ModuleTypeProfile',
) )
class ModuleTypeProfile(PrimaryModel):
"""
A profile which defines the attributes which can be set on one or more ModuleTypes.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
schema = models.JSONField(
verbose_name=_('schema')
)
clone_fields = ('schema',)
class Meta:
ordering = ('name',)
verbose_name = _('module type profile')
verbose_name_plural = _('module type profiles')
def __str__(self):
return self.name
class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
""" """
A ModuleType represents a hardware element that can be installed within a device and which houses additional A ModuleType represents a hardware element that can be installed within a device and which houses additional
@ -25,6 +50,13 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
DeviceType, each ModuleType can have console, power, interface, and pass-through port templates assigned to it. It 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. cannot, however house device bays or module bays.
""" """
profile = models.ForeignKey(
to='dcim.ModuleTypeProfile',
on_delete=models.PROTECT,
related_name='module_types',
blank=True,
null=True
)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
to='dcim.Manufacturer', to='dcim.Manufacturer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -47,14 +79,19 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
blank=True, blank=True,
null=True null=True
) )
attributes = models.JSONField(
blank=True,
null=True,
verbose_name=_('attributes')
)
clone_fields = ('manufacturer', 'weight', 'weight_unit', 'airflow') clone_fields = ('profile', 'manufacturer', 'weight', 'weight_unit', 'airflow')
prerequisite_models = ( prerequisite_models = (
'dcim.Manufacturer', 'dcim.Manufacturer',
) )
class Meta: class Meta:
ordering = ('manufacturer', 'model') ordering = ('profile', 'manufacturer', 'model')
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('manufacturer', 'model'), fields=('manufacturer', 'model'),
@ -73,6 +110,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
def to_yaml(self): def to_yaml(self):
data = { data = {
'profile': self.profile.name if self.profile else None,
'manufacturer': self.manufacturer.name, 'manufacturer': self.manufacturer.name,
'model': self.model, 'model': self.model,
'part_number': self.part_number, 'part_number': self.part_number,

View File

@ -183,6 +183,17 @@ class ModuleBayIndex(SearchIndex):
display_attrs = ('device', 'label', 'position', 'description') display_attrs = ('device', 'label', 'position', 'description')
@register_search
class ModuleTypeProfileIndex(SearchIndex):
model = models.ModuleTypeProfile
fields = (
('name', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('name', 'description')
@register_search @register_search
class ModuleTypeIndex(SearchIndex): class ModuleTypeIndex(SearchIndex):
model = models.ModuleType model = models.ModuleType

View File

@ -7,19 +7,42 @@ from .template_code import WEIGHT
__all__ = ( __all__ = (
'ModuleTable', 'ModuleTable',
'ModuleTypeProfileTable',
'ModuleTypeTable', 'ModuleTypeTable',
) )
class ModuleTypeProfileTable(NetBoxTable):
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='dcim:moduletype_list'
)
class Meta(NetBoxTable.Meta):
model = ModuleType
fields = (
'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'description',
)
class ModuleTypeTable(NetBoxTable): class ModuleTypeTable(NetBoxTable):
model = tables.Column( profile = tables.Column(
linkify=True, verbose_name=_('Profile'),
verbose_name=_('Module Type') linkify=True
) )
manufacturer = tables.Column( manufacturer = tables.Column(
verbose_name=_('Manufacturer'), verbose_name=_('Manufacturer'),
linkify=True linkify=True
) )
model = tables.Column(
linkify=True,
verbose_name=_('Module Type')
)
instance_count = columns.LinkedCountColumn( instance_count = columns.LinkedCountColumn(
viewname='dcim:module_list', viewname='dcim:module_list',
url_params={'module_type_id': 'pk'}, url_params={'module_type_id': 'pk'},
@ -40,11 +63,11 @@ class ModuleTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ModuleType model = ModuleType
fields = ( fields = (
'pk', 'id', 'model', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'comments', 'tags', 'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description',
'created', 'last_updated', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'pk', 'model', 'profile', 'manufacturer', 'part_number',
) )

View File

@ -591,7 +591,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
class ModuleTypeTest(APIViewTestCases.APIViewTestCase): class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
model = ModuleType model = ModuleType
brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'url'] brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'profile', 'url']
bulk_update_data = { bulk_update_data = {
'part_number': 'ABC123', 'part_number': 'ABC123',
} }

View File

@ -1486,6 +1486,7 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
filterset = ModuleTypeFilterSet filterset = ModuleTypeFilterSet
ignore_fields = ['attributes']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -37,6 +37,9 @@ urlpatterns = [
path('device-types/', include(get_model_urls('dcim', 'devicetype', detail=False))), path('device-types/', include(get_model_urls('dcim', 'devicetype', detail=False))),
path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))), path('device-types/<int:pk>/', include(get_model_urls('dcim', 'devicetype'))),
path('module-type-profiles/', include(get_model_urls('dcim', 'moduletypeprofile', detail=False))),
path('module-type-profiles/<int:pk>/', include(get_model_urls('dcim', 'moduletypeprofile'))),
path('module-types/', include(get_model_urls('dcim', 'moduletype', detail=False))), path('module-types/', include(get_model_urls('dcim', 'moduletype', detail=False))),
path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))), path('module-types/<int:pk>/', include(get_model_urls('dcim', 'moduletype'))),

View File

@ -1278,6 +1278,62 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceTypeTable table = tables.DeviceTypeTable
#
# Module type profiles
#
@register_model_view(ModuleTypeProfile, 'list', path='', detail=False)
class ModuleTypeProfileListView(generic.ObjectListView):
queryset = ModuleTypeProfile.objects.annotate(
instance_count=count_related(ModuleType, 'profile')
)
filterset = filtersets.ModuleTypeProfileFilterSet
filterset_form = forms.ModuleTypeProfileFilterForm
table = tables.ModuleTypeProfileTable
@register_model_view(ModuleTypeProfile)
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleTypeProfile.objects.all()
@register_model_view(ModuleTypeProfile, 'add', detail=False)
@register_model_view(ModuleTypeProfile, 'edit')
class ModuleTypeProfileEditView(generic.ObjectEditView):
queryset = ModuleTypeProfile.objects.all()
form = forms.ModuleTypeProfileForm
@register_model_view(ModuleTypeProfile, 'delete')
class ModuleTypeProfileDeleteView(generic.ObjectDeleteView):
queryset = ModuleTypeProfile.objects.all()
@register_model_view(ModuleTypeProfile, 'bulk_import', detail=False)
class ModuleTypeProfileBulkImportView(generic.BulkImportView):
queryset = ModuleTypeProfile.objects.all()
model_form = forms.ModuleTypeProfileImportForm
@register_model_view(ModuleTypeProfile, 'bulk_edit', path='edit', detail=False)
class ModuleTypeProfileBulkEditView(generic.BulkEditView):
queryset = ModuleTypeProfile.objects.annotate(
instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeProfileFilterSet
table = tables.ModuleTypeProfileTable
form = forms.ModuleTypeProfileBulkEditForm
@register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleTypeProfile.objects.annotate(
instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeProfileFilterSet
table = tables.ModuleTypeProfileTable
# #
# Module types # Module types
# #

View File

@ -1161,6 +1161,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
'module', 'module',
'modulebay', 'modulebay',
'moduletype', 'moduletype',
'moduletypeprofile',
'platform', 'platform',
'powerfeed', 'powerfeed',
'poweroutlet', 'poweroutlet',

View File

@ -85,6 +85,7 @@ DEVICES_MENU = Menu(
items=( items=(
get_model_item('dcim', 'devicetype', _('Device Types')), get_model_item('dcim', 'devicetype', _('Device Types')),
get_model_item('dcim', 'moduletype', _('Module Types')), get_model_item('dcim', 'moduletype', _('Module Types')),
get_model_item('dcim', 'moduletypeprofile', _('Module Type Profiles')),
get_model_item('dcim', 'manufacturer', _('Manufacturers')), get_model_item('dcim', 'manufacturer', _('Manufacturers')),
), ),
), ),

View File

@ -23,6 +23,10 @@
<div class="card"> <div class="card">
<h2 class="card-header">{% trans "Module Type" %}</h2> <h2 class="card-header">{% trans "Module Type" %}</h2>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Profile" %}</th>
<td>{{ object.profile|linkify }}</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Manufacturer" %}</th> <th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify }}</td> <td>{{ object.manufacturer|linkify }}</td>

View File

@ -0,0 +1,44 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object.name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Type Profile" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Schema" %}</h2>
<pre>{{ object.schema|json }}</pre>
</div>
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.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

@ -20,6 +20,7 @@ drf-spectacular-sidecar==2025.2.1
feedparser==6.0.11 feedparser==6.0.11
gunicorn==23.0.0 gunicorn==23.0.0
Jinja2==3.1.5 Jinja2==3.1.5
jsonschema==4.23.0
Markdown==3.7 Markdown==3.7
mkdocs-material==9.6.7 mkdocs-material==9.6.7
mkdocstrings[python]==0.28.2 mkdocstrings[python]==0.28.2