From 24b957aab404a65eeff82516315b45579a4ac7e4 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Tue, 25 Jun 2024 10:02:57 -0700 Subject: [PATCH] 12826 add forms, filters, tables --- netbox/dcim/filtersets.py | 36 ++++++++++++++ netbox/dcim/forms/bulk_edit.py | 84 ++++++++++++++++++++++++++++++++ netbox/dcim/forms/bulk_import.py | 45 +++++++++++++++++ netbox/dcim/forms/filtersets.py | 48 ++++++++++++++++++ netbox/dcim/forms/model_forms.py | 28 +++++++++++ netbox/dcim/tables/racks.py | 58 +++++++++++++++++++++- netbox/dcim/urls.py | 8 +++ netbox/dcim/views.py | 46 +++++++++++++++++ 8 files changed, 352 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index a4d75654e..612b6e858 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -69,6 +69,7 @@ __all__ = ( 'RackFilterSet', 'RackReservationFilterSet', 'RackRoleFilterSet', + 'RackTypeFilterSet', 'RearPortFilterSet', 'RearPortTemplateFilterSet', 'RegionFilterSet', @@ -289,6 +290,41 @@ class RackRoleFilterSet(OrganizationalModelFilterSet): fields = ('id', 'name', 'slug', 'color', 'description') +class RackTypeFilterSet(NetBoxModelFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=RackTypeChoices + ) + width = django_filters.MultipleChoiceFilter( + choices=RackWidthChoices + ) + role_id = django_filters.ModelMultipleChoiceFilter( + queryset=RackRole.objects.all(), + label=_('Role (ID)'), + ) + role = django_filters.ModelMultipleChoiceFilter( + field_name='role__slug', + queryset=RackRole.objects.all(), + to_field_name='slug', + label=_('Role (slug)'), + ) + + class Meta: + model = Rack + fields = ( + 'id', 'name', 'u_height', 'starting_unit', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', '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 RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 25b049e6d..964749ad5 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -52,6 +52,7 @@ __all__ = ( 'RackBulkEditForm', 'RackReservationBulkEditForm', 'RackRoleBulkEditForm', + 'RackTypeBulkEditForm', 'RearPortBulkEditForm', 'RearPortTemplateBulkEditForm', 'RegionBulkEditForm', @@ -218,6 +219,89 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('color', 'description') +class RackTypeBulkEditForm(NetBoxModelBulkEditForm): + role = DynamicModelChoiceField( + label=_('Role'), + queryset=RackRole.objects.all(), + required=False + ) + type = forms.ChoiceField( + label=_('Type'), + choices=add_blank_choice(RackTypeChoices), + required=False + ) + width = forms.ChoiceField( + label=_('Width'), + choices=add_blank_choice(RackWidthChoices), + required=False + ) + u_height = forms.IntegerField( + required=False, + label=_('Height (U)') + ) + desc_units = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect, + label=_('Descending units') + ) + outer_width = forms.IntegerField( + label=_('Outer width'), + required=False, + min_value=1 + ) + outer_depth = forms.IntegerField( + label=_('Outer depth'), + required=False, + min_value=1 + ) + outer_unit = forms.ChoiceField( + label=_('Outer unit'), + choices=add_blank_choice(RackDimensionUnitChoices), + required=False + ) + mounting_depth = forms.IntegerField( + label=_('Mounting depth'), + required=False, + min_value=1 + ) + weight = forms.DecimalField( + label=_('Weight'), + min_value=0, + required=False + ) + max_weight = forms.IntegerField( + label=_('Max weight'), + min_value=0, + required=False + ) + weight_unit = forms.ChoiceField( + label=_('Weight unit'), + choices=add_blank_choice(WeightUnitChoices), + required=False, + initial='' + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() + + model = Rack + fieldsets = ( + FieldSet('role', 'description', name=_('Rack')), + FieldSet( + 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', + name=_('Hardware') + ), + FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), + ) + nullable_fields = ( + 'role', 'outer_width', 'outer_depth', 'outer_unit', 'weight', + 'max_weight', 'weight_unit', 'description', 'comments', + ) + + class RackBulkEditForm(NetBoxModelBulkEditForm): region = DynamicModelChoiceField( label=_('Region'), diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 5a64cad02..1440cc102 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -45,6 +45,7 @@ __all__ = ( 'RackImportForm', 'RackReservationImportForm', 'RackRoleImportForm', + 'RackTypeImportForm', 'RearPortImportForm', 'RegionImportForm', 'SiteImportForm', @@ -179,6 +180,50 @@ class RackRoleImportForm(NetBoxModelImportForm): } +class RackTypeImportForm(NetBoxModelImportForm): + role = CSVModelChoiceField( + label=_('Role'), + queryset=RackRole.objects.all(), + required=False, + to_field_name='name', + help_text=_('Name of assigned role') + ) + type = CSVChoiceField( + label=_('Type'), + choices=RackTypeChoices, + required=False, + help_text=_('Rack type') + ) + width = forms.ChoiceField( + label=_('Width'), + choices=RackWidthChoices, + help_text=_('Rail-to-rail width (in inches)') + ) + outer_unit = CSVChoiceField( + label=_('Outer unit'), + choices=RackDimensionUnitChoices, + required=False, + help_text=_('Unit for outer dimensions') + ) + weight_unit = CSVChoiceField( + label=_('Weight unit'), + choices=WeightUnitChoices, + required=False, + help_text=_('Unit for rack weights') + ) + + class Meta: + model = RackType + fields = ( + 'name', 'role', 'type', + 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', + 'max_weight', 'weight_unit', 'description', 'comments', 'tags', + ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + class RackImportForm(NetBoxModelImportForm): site = CSVModelChoiceField( label=_('Site'), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 0a28a4ec4..d25f4d5cf 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -47,6 +47,7 @@ __all__ = ( 'RackElevationFilterForm', 'RackReservationFilterForm', 'RackRoleFilterForm', + 'RackTypeFilterForm', 'RearPortFilterForm', 'RegionFilterForm', 'SiteFilterForm', @@ -239,6 +240,53 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) +class RackTypeFilterForm(NetBoxModelFilterSetForm): + model = Rack + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('role_id', name=_('Function')), + FieldSet('type', 'width', 'serial', name=_('Hardware')), + FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), + ) + selector_fields = ('filter_id', 'q',) + type = forms.MultipleChoiceField( + label=_('Type'), + choices=RackTypeChoices, + required=False + ) + width = forms.MultipleChoiceField( + label=_('Width'), + choices=RackWidthChoices, + required=False + ) + role_id = DynamicModelMultipleChoiceField( + queryset=RackRole.objects.all(), + required=False, + null_option='None', + label=_('Role') + ) + serial = forms.CharField( + label=_('Serial'), + required=False + ) + tag = TagFilterField(model) + weight = forms.DecimalField( + label=_('Weight'), + required=False, + min_value=1 + ) + max_weight = forms.IntegerField( + label=_('Max weight'), + required=False, + min_value=1 + ) + weight_unit = forms.ChoiceField( + label=_('Weight unit'), + choices=add_blank_choice(WeightUnitChoices), + required=False + ) + + class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Rack fieldsets = ( diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index d493687f9..f6ab0c1bf 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -57,6 +57,7 @@ __all__ = ( 'RackForm', 'RackReservationForm', 'RackRoleForm', + 'RackTypeForm', 'RearPortForm', 'RearPortTemplateForm', 'RegionForm', @@ -201,6 +202,33 @@ class RackRoleForm(NetBoxModelForm): ] +class RackTypeForm(NetBoxModelForm): + role = DynamicModelChoiceField( + label=_('Role'), + queryset=RackRole.objects.all(), + required=False + ) + comments = CommentField() + + fieldsets = ( + FieldSet('name', 'status', 'role', 'description', 'tags', name=_('Rack')), + FieldSet( + 'type', 'width', 'starting_unit', 'u_height', + InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), + InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), + 'mounting_depth', 'desc_units', name=_('Dimensions') + ), + ) + + class Meta: + model = Rack + fields = [ + 'name', 'role', + 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', + 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', + ] + + class RackForm(TenancyForm, NetBoxModelForm): site = DynamicModelChoiceField( label=_('Site'), diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 22ca3da90..4da1a3db2 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from django_tables2.utils import Accessor -from dcim.models import Rack, RackReservation, RackRole +from dcim.models import Rack, RackReservation, RackRole, RackType from netbox.tables import NetBoxTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from .template_code import WEIGHT @@ -11,6 +11,7 @@ __all__ = ( 'RackTable', 'RackReservationTable', 'RackRoleTable', + 'RackTypeTable', ) @@ -44,6 +45,61 @@ class RackRoleTable(NetBoxTable): default_columns = ('pk', 'name', 'rack_count', 'color', 'description') +# +# Rack Types +# + +class RackTypeTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + order_by=('_name',), + linkify=True + ) + role = columns.ColoredLabelColumn( + verbose_name=_('Role'), + ) + u_height = tables.TemplateColumn( + template_code="{{ value }}U", + verbose_name=_('Height') + ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) + tags = columns.TagColumn( + url_name='dcim:rack_list' + ) + outer_width = tables.TemplateColumn( + template_code="{{ record.outer_width }} {{ record.outer_unit }}", + verbose_name=_('Outer Width') + ) + outer_depth = tables.TemplateColumn( + template_code="{{ record.outer_depth }} {{ record.outer_unit }}", + verbose_name=_('Outer Depth') + ) + weight = columns.TemplateColumn( + verbose_name=_('Weight'), + template_code=WEIGHT, + order_by=('_abs_weight', 'weight_unit') + ) + max_weight = columns.TemplateColumn( + verbose_name=_('Max Weight'), + template_code=WEIGHT, + order_by=('_abs_max_weight', 'weight_unit') + ) + + class Meta(NetBoxTable.Meta): + model = RackType + fields = ( + 'pk', 'id', 'name', 'role', + 'type', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', 'mounting_depth', + 'weight', 'max_weight', 'comments', + 'description', 'tags', 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'role', 'u_height', + ) + + # # Racks # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c71a0aff1..1dff67ca3 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -63,6 +63,14 @@ urlpatterns = [ path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), path('racks//', include(get_model_urls('dcim', 'rack'))), + # Rack Types + path('racktypes/', views.RackTypeListView.as_view(), name='racktype_list'), + path('racktypes/add/', views.RackTypeEditView.as_view(), name='racktype_add'), + path('racktypes/import/', views.RackTypeBulkImportView.as_view(), name='racktype_import'), + path('racktypes/edit/', views.RackTypeBulkEditView.as_view(), name='racktype_bulk_edit'), + path('racktypes/delete/', views.RackTypeBulkDeleteView.as_view(), name='racktype_bulk_delete'), + path('racktypes//', include(get_model_urls('dcim', 'racktype'))), + # Manufacturers path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 87f351e4d..695ed3054 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -578,6 +578,52 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): table = tables.RackRoleTable +# +# RackTypes +# + +class RackTypeListView(generic.ObjectListView): + queryset = RackType.objects.all() + filterset = filtersets.RackTypeFilterSet + filterset_form = forms.RackTypeFilterForm + table = tables.RackTypeTable + template_name = 'dcim/racktype_list.html' + + +@register_model_view(RackType) +class RackTypeView(GetRelatedModelsMixin, generic.ObjectView): + queryset = Rack.objects.prefetch_related('role') + + +@register_model_view(RackType, 'edit') +class RackTypeEditView(generic.ObjectEditView): + queryset = RackType.objects.all() + form = forms.RackTypeForm + + +@register_model_view(RackType, 'delete') +class RackTypeDeleteView(generic.ObjectDeleteView): + queryset = RackType.objects.all() + + +class RackTypeBulkImportView(generic.BulkImportView): + queryset = RackType.objects.all() + model_form = forms.RackTypeImportForm + + +class RackTypeBulkEditView(generic.BulkEditView): + queryset = RackType.objects.all() + filterset = filtersets.RackTypeFilterSet + table = tables.RackTypeTable + form = forms.RackTypeBulkEditForm + + +class RackTypeBulkDeleteView(generic.BulkDeleteView): + queryset = RackType.objects.all() + filterset = filtersets.RackTypeFilterSet + table = tables.RackTypeTable + + # # Racks #