From 678e6f1bd5400b780729bf920a85076b31ac0e68 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Mar 2025 16:27:07 -0400 Subject: [PATCH] Add ModuleTypeProfile & related fields --- base_requirements.txt | 4 ++ netbox/dcim/api/serializers_/devicetypes.py | 27 +++++++-- netbox/dcim/api/urls.py | 1 + netbox/dcim/api/views.py | 6 ++ netbox/dcim/filtersets.py | 27 +++++++++ netbox/dcim/forms/bulk_edit.py | 27 ++++++++- netbox/dcim/forms/bulk_import.py | 16 ++++++ netbox/dcim/forms/filtersets.py | 16 +++++- netbox/dcim/forms/model_forms.py | 29 ++++++++-- netbox/dcim/graphql/filters.py | 6 ++ netbox/dcim/graphql/schema.py | 3 + netbox/dcim/graphql/types.py | 12 ++++ .../dcim/migrations/0203_moduletypeprofile.py | 57 +++++++++++++++++++ netbox/dcim/models/modules.py | 42 +++++++++++++- netbox/dcim/search.py | 11 ++++ netbox/dcim/tables/modules.py | 35 ++++++++++-- netbox/dcim/tests/test_api.py | 2 +- netbox/dcim/tests/test_filtersets.py | 1 + netbox/dcim/urls.py | 3 + netbox/dcim/views.py | 56 ++++++++++++++++++ netbox/extras/tests/test_filtersets.py | 1 + netbox/netbox/navigation/menu.py | 1 + netbox/templates/dcim/moduletype.html | 4 ++ netbox/templates/dcim/moduletypeprofile.html | 44 ++++++++++++++ requirements.txt | 1 + 25 files changed, 412 insertions(+), 20 deletions(-) create mode 100644 netbox/dcim/migrations/0203_moduletypeprofile.py create mode 100644 netbox/templates/dcim/moduletypeprofile.html diff --git a/base_requirements.txt b/base_requirements.txt index 6921f2d49..7eaa9d928 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -82,6 +82,10 @@ gunicorn # https://jinja.palletsprojects.com/changes/ Jinja2 +# JSON schema validation +# https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst +jsonschema + # Simple markup language for rendering HTML # https://python-markdown.github.io/changelog/ Markdown diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index 0ce2af2f8..1ac453434 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext as _ from rest_framework import serializers 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.serializers import NetBoxModelSerializer from netbox.choices import * @@ -13,6 +13,7 @@ from .platforms import PlatformSerializer __all__ = ( 'DeviceTypeSerializer', + 'ModuleTypeProfileSerializer', 'ModuleTypeSerializer', ) @@ -62,7 +63,23 @@ class DeviceTypeSerializer(NetBoxModelSerializer): 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): + profile = ModuleTypeProfileSerializer( + nested=True, + required=False, + allow_null=True + ) manufacturer = ManufacturerSerializer( nested=True ) @@ -82,8 +99,8 @@ class ModuleTypeSerializer(NetBoxModelSerializer): class Meta: model = ModuleType fields = [ - 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow', - 'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow', + 'weight', 'weight_unit', 'description', 'attributes', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') + brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description') diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index fc3740374..734ac13db 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -21,6 +21,7 @@ router.register('rack-reservations', views.RackReservationViewSet) router.register('manufacturers', views.ManufacturerViewSet) router.register('device-types', views.DeviceTypeViewSet) router.register('module-types', views.ModuleTypeViewSet) +router.register('module-type-profiles', views.ModuleTypeProfileViewSet) # Device type components router.register('console-port-templates', views.ConsolePortTemplateViewSet) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d7dbbef91..575ee770f 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -269,6 +269,12 @@ class DeviceTypeViewSet(NetBoxModelViewSet): filterset_class = filtersets.DeviceTypeFilterSet +class ModuleTypeProfileViewSet(NetBoxModelViewSet): + queryset = ModuleTypeProfile.objects.all() + serializer_class = serializers.ModuleTypeProfileSerializer + filterset_class = filtersets.ModuleTypeProfileFilterSet + + class ModuleTypeViewSet(NetBoxModelViewSet): queryset = ModuleType.objects.all() serializer_class = serializers.ModuleTypeSerializer diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 2c8b77f57..fa6837418 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -59,6 +59,7 @@ __all__ = ( 'ModuleBayTemplateFilterSet', 'ModuleFilterSet', 'ModuleTypeFilterSet', + 'ModuleTypeProfileFilterSet', 'PathEndpointFilterSet', 'PlatformFilterSet', 'PowerConnectionFilterSet', @@ -674,7 +675,33 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet): 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): + 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( queryset=Manufacturer.objects.all(), label=_('Manufacturer (ID)'), diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index a77c7fa9c..e49674f8f 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -46,6 +46,7 @@ __all__ = ( 'ModuleBayBulkEditForm', 'ModuleBayTemplateBulkEditForm', 'ModuleTypeBulkEditForm', + 'ModuleTypeProfileBulkEditForm', 'PlatformBulkEditForm', 'PowerFeedBulkEditForm', 'PowerOutletBulkEditForm', @@ -574,7 +575,31 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): 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): + profile = DynamicModelChoiceField( + label=_('Profile'), + queryset=ModuleTypeProfile.objects.all(), + required=False + ) manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -609,7 +634,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): model = ModuleType fieldsets = ( - FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')), + FieldSet('profile', 'manufacturer', 'part_number', 'description', name=_('Module Type')), FieldSet( 'airflow', InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 081e9d41d..d412694b3 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -39,6 +39,7 @@ __all__ = ( 'ModuleImportForm', 'ModuleBayImportForm', 'ModuleTypeImportForm', + 'ModuleTypeProfileImportForm', 'PlatformImportForm', 'PowerFeedImportForm', 'PowerOutletImportForm', @@ -427,7 +428,22 @@ class DeviceTypeImportForm(NetBoxModelImportForm): ] +class ModuleTypeProfileImportForm(NetBoxModelImportForm): + + class Meta: + model = ModuleTypeProfile + fields = [ + 'name', 'description', 'schema', 'comments', 'tags', + ] + + class ModuleTypeImportForm(NetBoxModelImportForm): + profile = forms.ModelChoiceField( + label=_('Profile'), + queryset=ModuleTypeProfile.objects.all(), + to_field_name='name', + required=False + ) manufacturer = forms.ModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index b70661348..8465f6404 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -39,6 +39,7 @@ __all__ = ( 'ModuleFilterForm', 'ModuleBayFilterForm', 'ModuleTypeFilterForm', + 'ModuleTypeProfileFilterForm', 'PlatformFilterForm', 'PowerConnectionFilterForm', '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): model = ModuleType fieldsets = ( FieldSet('q', 'filter_id', 'tag'), - FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')), + FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')), FieldSet( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', name=_('Components') @@ -614,6 +623,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): FieldSet('weight', 'weight_unit', name=_('Weight')), ) selector_fields = ('filter_id', 'q', 'manufacturer_id') + profile_id = DynamicModelMultipleChoiceField( + queryset=ModuleTypeProfile.objects.all(), + required=False, + label=_('Profile') + ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 2829ac754..d5b84649e 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -48,6 +48,7 @@ __all__ = ( 'ModuleBayForm', 'ModuleBayTemplateForm', 'ModuleTypeForm', + 'ModuleTypeProfileForm', 'PlatformForm', 'PopulateDeviceBayForm', '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): + profile = DynamicModelChoiceField( + label=_('Profile'), + queryset=ModuleTypeProfile.objects.all(), + required=False + ) manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all() @@ -412,15 +432,16 @@ class ModuleTypeForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), - FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis')) + FieldSet('profile', 'manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), + FieldSet('attributes', name=_('Profile Attributes')), + FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')), ) class Meta: model = ModuleType fields = [ - 'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description', - 'comments', 'tags', + 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', + 'attributes', 'comments', 'tags', ] diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 4203517cc..5dfc0d73c 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -68,6 +68,7 @@ __all__ = ( 'ModuleBayFilter', 'ModuleBayTemplateFilter', 'ModuleTypeFilter', + 'ModuleTypeProfileFilter', 'PlatformFilter', 'PowerFeedFilter', 'PowerOutletFilter', @@ -559,6 +560,11 @@ class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin): 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) class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index 011a2b58b..1b0661bc2 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -77,6 +77,9 @@ class DCIMQuery: module_bay_template: 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_list: List[ModuleTypeType] = strawberry_django.field() diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 24fa16263..b1df64a03 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -61,6 +61,7 @@ __all__ = ( 'ModuleType', 'ModuleBayType', 'ModuleBayTemplateType', + 'ModuleTypeProfileType', 'ModuleTypeType', 'PlatformType', 'PowerFeedType', @@ -593,6 +594,16 @@ class ModuleBayTemplateType(ModularComponentTemplateType): 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( models.ModuleType, fields='__all__', @@ -600,6 +611,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType): pagination=True ) class ModuleTypeType(NetBoxObjectType): + profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] diff --git a/netbox/dcim/migrations/0203_moduletypeprofile.py b/netbox/dcim/migrations/0203_moduletypeprofile.py new file mode 100644 index 000000000..69c373340 --- /dev/null +++ b/netbox/dcim/migrations/0203_moduletypeprofile.py @@ -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')}, + ), + ] diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 85463fe72..a41c216be 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -15,9 +15,34 @@ from .device_components import * __all__ = ( 'Module', '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): """ 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 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( to='dcim.Manufacturer', on_delete=models.PROTECT, @@ -47,14 +79,19 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): blank=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 = ( 'dcim.Manufacturer', ) class Meta: - ordering = ('manufacturer', 'model') + ordering = ('profile', 'manufacturer', 'model') constraints = ( models.UniqueConstraint( fields=('manufacturer', 'model'), @@ -73,6 +110,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): def to_yaml(self): data = { + 'profile': self.profile.name if self.profile else None, 'manufacturer': self.manufacturer.name, 'model': self.model, 'part_number': self.part_number, diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index a85005679..33b666df7 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -183,6 +183,17 @@ class ModuleBayIndex(SearchIndex): 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 class ModuleTypeIndex(SearchIndex): model = models.ModuleType diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index 6bd0d53b5..4fc6d866c 100644 --- a/netbox/dcim/tables/modules.py +++ b/netbox/dcim/tables/modules.py @@ -7,19 +7,42 @@ from .template_code import WEIGHT __all__ = ( 'ModuleTable', + 'ModuleTypeProfileTable', '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): - model = tables.Column( - linkify=True, - verbose_name=_('Module Type') + profile = tables.Column( + verbose_name=_('Profile'), + linkify=True ) manufacturer = tables.Column( verbose_name=_('Manufacturer'), linkify=True ) + model = tables.Column( + linkify=True, + verbose_name=_('Module Type') + ) instance_count = columns.LinkedCountColumn( viewname='dcim:module_list', url_params={'module_type_id': 'pk'}, @@ -40,11 +63,11 @@ class ModuleTypeTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ModuleType fields = ( - 'pk', 'id', 'model', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'comments', 'tags', - 'created', 'last_updated', + 'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', + 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'model', 'manufacturer', 'part_number', + 'pk', 'model', 'profile', 'manufacturer', 'part_number', ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index c2a7660c6..42938af81 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -591,7 +591,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase): class ModuleTypeTest(APIViewTestCases.APIViewTestCase): model = ModuleType - brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'url'] + brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'profile', 'url'] bulk_update_data = { 'part_number': 'ABC123', } diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index b2353b4ba..5dd5534bc 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1486,6 +1486,7 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ModuleType.objects.all() filterset = ModuleTypeFilterSet + ignore_fields = ['attributes'] @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index bcfd32707..122593834 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -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'))), + path('module-type-profiles/', include(get_model_urls('dcim', 'moduletypeprofile', detail=False))), + path('module-type-profiles//', 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'))), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0978747d1..7678d4f94 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1278,6 +1278,62 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView): 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 # diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 84d7aad5a..247653472 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1161,6 +1161,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): 'module', 'modulebay', 'moduletype', + 'moduletypeprofile', 'platform', 'powerfeed', 'poweroutlet', diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 9148caa8e..778f0d67c 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -85,6 +85,7 @@ DEVICES_MENU = Menu( items=( get_model_item('dcim', 'devicetype', _('Device Types')), get_model_item('dcim', 'moduletype', _('Module Types')), + get_model_item('dcim', 'moduletypeprofile', _('Module Type Profiles')), get_model_item('dcim', 'manufacturer', _('Manufacturers')), ), ), diff --git a/netbox/templates/dcim/moduletype.html b/netbox/templates/dcim/moduletype.html index b3d53e09b..2b1039abb 100644 --- a/netbox/templates/dcim/moduletype.html +++ b/netbox/templates/dcim/moduletype.html @@ -23,6 +23,10 @@

{% trans "Module Type" %}

+ + + + diff --git a/netbox/templates/dcim/moduletypeprofile.html b/netbox/templates/dcim/moduletypeprofile.html new file mode 100644 index 000000000..7277fdc87 --- /dev/null +++ b/netbox/templates/dcim/moduletypeprofile.html @@ -0,0 +1,44 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load helpers %} +{% load plugins %} +{% load i18n %} + +{% block title %}{{ object.name }}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Module Type Profile" %}

+
{% trans "Profile" %}{{ object.profile|linkify }}
{% trans "Manufacturer" %} {{ object.manufacturer|linkify }}
+ + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
+
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_left_page object %} + +
+
+

{% trans "Schema" %}

+
{{ object.schema|json }}
+
+ {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %} +
+ +
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/requirements.txt b/requirements.txt index 210a0b1d8..06e0e1617 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ drf-spectacular-sidecar==2025.2.1 feedparser==6.0.11 gunicorn==23.0.0 Jinja2==3.1.5 +jsonschema==4.23.0 Markdown==3.7 mkdocs-material==9.6.7 mkdocstrings[python]==0.28.2