Przeglądaj źródła

Use FieldSet instances for all forms

Jeremy Stretch 1 rok temu
rodzic
commit
72d3c17b48
35 zmienionych plików z 800 dodań i 757 usunięć
  1. 8 7
      netbox/circuits/forms/bulk_edit.py
  2. 17 16
      netbox/circuits/forms/filtersets.py
  3. 14 19
      netbox/circuits/forms/model_forms.py
  4. 2 1
      netbox/core/forms/bulk_edit.py
  5. 12 11
      netbox/core/forms/filtersets.py
  6. 22 15
      netbox/core/forms/model_forms.py
  7. 58 55
      netbox/dcim/forms/bulk_edit.py
  8. 133 125
      netbox/dcim/forms/filtersets.py
  9. 101 106
      netbox/dcim/forms/model_forms.py
  10. 4 3
      netbox/dcim/forms/object_create.py
  11. 35 34
      netbox/extras/forms/filtersets.py
  12. 40 31
      netbox/extras/forms/model_forms.py
  13. 23 20
      netbox/ipam/forms/bulk_edit.py
  14. 50 43
      netbox/ipam/forms/filtersets.py
  15. 56 58
      netbox/ipam/forms/model_forms.py
  16. 1 1
      netbox/netbox/forms/base.py
  17. 7 3
      netbox/templates/generic/bulk_edit.html
  18. 4 4
      netbox/templates/inc/filter_list.html
  19. 6 5
      netbox/tenancy/forms/bulk_edit.py
  20. 5 4
      netbox/tenancy/forms/filtersets.py
  21. 10 13
      netbox/tenancy/forms/model_forms.py
  22. 4 3
      netbox/users/forms/bulk_edit.py
  23. 10 9
      netbox/users/forms/filtersets.py
  24. 17 21
      netbox/users/forms/model_forms.py
  25. 5 4
      netbox/utilities/forms/rendering.py
  26. 6 0
      netbox/utilities/templatetags/form_helpers.py
  27. 12 11
      netbox/virtualization/forms/bulk_edit.py
  28. 23 19
      netbox/virtualization/forms/filtersets.py
  29. 17 20
      netbox/virtualization/forms/model_forms.py
  30. 12 15
      netbox/vpn/forms/bulk_edit.py
  31. 26 22
      netbox/vpn/forms/filtersets.py
  32. 35 35
      netbox/vpn/forms/model_forms.py
  33. 6 5
      netbox/wireless/forms/bulk_edit.py
  34. 9 8
      netbox/wireless/forms/filtersets.py
  35. 10 11
      netbox/wireless/forms/model_forms.py

+ 8 - 7
netbox/circuits/forms/bulk_edit.py

@@ -8,6 +8,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 
 
 __all__ = (
 __all__ = (
@@ -34,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Provider
     model = Provider
     fieldsets = (
     fieldsets = (
-        (None, ('asns', 'description')),
+        FieldSet('asns', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'asns', 'description', 'comments',
         'asns', 'description', 'comments',
@@ -56,7 +57,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ProviderAccount
     model = ProviderAccount
     fieldsets = (
     fieldsets = (
-        (None, ('provider', 'description')),
+        FieldSet('provider', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'description', 'comments',
         'description', 'comments',
@@ -83,7 +84,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ProviderNetwork
     model = ProviderNetwork
     fieldsets = (
     fieldsets = (
-        (None, ('provider', 'service_id', 'description')),
+        FieldSet('provider', 'service_id', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'service_id', 'description', 'comments',
         'service_id', 'description', 'comments',
@@ -103,7 +104,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = CircuitType
     model = CircuitType
     fieldsets = (
     fieldsets = (
-        (None, ('color', 'description')),
+        FieldSet('color', 'description'),
     )
     )
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
@@ -164,9 +165,9 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Circuit
     model = Circuit
     fieldsets = (
     fieldsets = (
-        (_('Circuit'), ('provider', 'type', 'status', 'description')),
-        (_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
-        (_('Tenancy'), ('tenant',)),
+        FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
+        FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
+        FieldSet('tenant', name=_('Tenancy')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'tenant', 'commit_rate', 'description', 'comments',
         'tenant', 'commit_rate', 'description', 'comments',

+ 17 - 16
netbox/circuits/forms/filtersets.py

@@ -8,6 +8,7 @@ from ipam.models import ASN
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 
 
 __all__ = (
 __all__ = (
@@ -22,10 +23,10 @@ __all__ = (
 class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Provider
     model = Provider
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('ASN'), ('asn',)),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('asn', name=_('ASN')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -61,8 +62,8 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
 class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
     model = ProviderAccount
     model = ProviderAccount
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('provider_id', 'account')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('provider_id', 'account', name=_('Attributes')),
     )
     )
     provider_id = DynamicModelMultipleChoiceField(
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -79,8 +80,8 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
 class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
 class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
     model = ProviderNetwork
     model = ProviderNetwork
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('provider_id', 'service_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('provider_id', 'service_id', name=_('Attributes')),
     )
     )
     provider_id = DynamicModelMultipleChoiceField(
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -98,8 +99,8 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
 class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
 class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
     model = CircuitType
     model = CircuitType
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('color',)),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('color', name=_('Attributes')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
@@ -112,12 +113,12 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
 class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
 class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Circuit
     model = Circuit
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')),
-        (_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
+        FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
     type_id = DynamicModelMultipleChoiceField(
     type_id = DynamicModelMultipleChoiceField(

+ 14 - 19
netbox/circuits/forms/model_forms.py

@@ -7,7 +7,7 @@ from ipam.models import ASN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
-from utilities.forms.rendering import TabbedGroups
+from utilities.forms.rendering import FieldSet, TabbedGroups
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 
 
 __all__ = (
 __all__ = (
@@ -30,7 +30,7 @@ class ProviderForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')),
+        FieldSet('name', 'slug', 'asns', 'description', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -62,7 +62,7 @@ class ProviderNetworkForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')),
+        FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -76,9 +76,7 @@ class CircuitTypeForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Circuit Type'), (
-            'name', 'slug', 'color', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'color', 'description', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -108,9 +106,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
-        (_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
+        FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -148,18 +146,15 @@ class CircuitTerminationForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Circuit Termination'), (
-            'circuit',
-            'term_side',
-            'description',
-            'tags',
+        FieldSet(
+            'circuit', 'term_side', 'description', 'tags',
             TabbedGroups(
             TabbedGroups(
-                (_('Site'), 'site'),
-                (_('Provider Network'), 'provider_network'),
+                FieldSet('site', name=_('Site')),
+                FieldSet('provider_network', name=_('Provider Network')),
             ),
             ),
-            'mark_connected',
-        )),
-        (_('Termination Details'), ('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info')),
+            'mark_connected', name=_('Circuit Termination')
+        ),
+        FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
     )
     )
 
 
     class Meta:
     class Meta:

+ 2 - 1
netbox/core/forms/bulk_edit.py

@@ -5,6 +5,7 @@ from core.models import *
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from utilities.forms.fields import CommentField
 from utilities.forms.fields import CommentField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 
 
 __all__ = (
 __all__ = (
@@ -41,7 +42,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = DataSource
     model = DataSource
     fieldsets = (
     fieldsets = (
-        (None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')),
+        FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules',
         'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules',

+ 12 - 11
netbox/core/forms/filtersets.py

@@ -9,7 +9,8 @@ from netbox.forms.mixins import SavedFiltersMixin
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
-from utilities.forms.widgets import APISelectMultiple, DateTimePicker
+from utilities.forms.rendering import FieldSet
+from utilities.forms.widgets import DateTimePicker
 
 
 __all__ = (
 __all__ = (
     'ConfigRevisionFilterForm',
     'ConfigRevisionFilterForm',
@@ -22,8 +23,8 @@ __all__ = (
 class DataSourceFilterForm(NetBoxModelFilterSetForm):
 class DataSourceFilterForm(NetBoxModelFilterSetForm):
     model = DataSource
     model = DataSource
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Data Source'), ('type', 'status')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('type', 'status', name=_('Data Source')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -47,8 +48,8 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
 class DataFileFilterForm(NetBoxModelFilterSetForm):
 class DataFileFilterForm(NetBoxModelFilterSetForm):
     model = DataFile
     model = DataFile
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('File'), ('source_id',)),
+        FieldSet('q', 'filter_id'),
+        FieldSet('source_id', name=_('File')),
     )
     )
     source_id = DynamicModelMultipleChoiceField(
     source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
         queryset=DataSource.objects.all(),
@@ -59,12 +60,12 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
 
 
 class JobFilterForm(SavedFiltersMixin, FilterForm):
 class JobFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Attributes'), ('object_type', 'status')),
-        (_('Creation'), (
+        FieldSet('q', 'filter_id'),
+        FieldSet('object_type', 'status', name=_('Attributes')),
+        FieldSet(
             'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
             'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
-            'started__after', 'completed__before', 'completed__after', 'user',
-        )),
+            'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
+        ),
     )
     )
     object_type = ContentTypeChoiceField(
     object_type = ContentTypeChoiceField(
         label=_('Object Type'),
         label=_('Object Type'),
@@ -125,5 +126,5 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
+        FieldSet('q', 'filter_id'),
     )
     )

+ 22 - 15
netbox/core/forms/model_forms.py

@@ -13,6 +13,7 @@ from netbox.registry import registry
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from utilities.forms import get_field_value
 from utilities.forms import get_field_value
 from utilities.forms.fields import CommentField
 from utilities.forms.fields import CommentField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import HTMXSelect
 from utilities.forms.widgets import HTMXSelect
 
 
 __all__ = (
 __all__ = (
@@ -49,11 +50,11 @@ class DataSourceForm(NetBoxModelForm):
     @property
     @property
     def fieldsets(self):
     def fieldsets(self):
         fieldsets = [
         fieldsets = [
-            (_('Source'), ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
+            FieldSet('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules', name=_('Source')),
         ]
         ]
         if self.backend_fields:
         if self.backend_fields:
             fieldsets.append(
             fieldsets.append(
-                (_('Backend Parameters'), self.backend_fields)
+                FieldSet(*self.backend_fields, name=_('Backend Parameters'))
             )
             )
 
 
         return fieldsets
         return fieldsets
@@ -91,8 +92,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('File Upload'), ('upload_file',)),
-        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
+        FieldSet('upload_file', name=_('File Upload')),
+        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -144,18 +145,24 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
     """
     """
 
 
     fieldsets = (
     fieldsets = (
-        (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
-        (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
-        (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
-        (_('Security'), ('ALLOWED_URL_SCHEMES',)),
-        (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
-        (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
-        (_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
-        (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
-        (_('Miscellaneous'), (
+        FieldSet(
+            'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', name=_('Rack Elevations')
+        ),
+        FieldSet(
+            'POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION',
+            name=_('Power')
+        ),
+        FieldSet('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4', name=_('IPAM')),
+        FieldSet('ALLOWED_URL_SCHEMES', name=_('Security')),
+        FieldSet('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM', name=_('Banners')),
+        FieldSet('PAGINATE_COUNT', 'MAX_PAGE_SIZE', name=_('Pagination')),
+        FieldSet('CUSTOM_VALIDATORS', 'PROTECTION_RULES', name=_('Validation')),
+        FieldSet('DEFAULT_USER_PREFERENCES', name=_('User Preferences')),
+        FieldSet(
             'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
             'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
-        )),
-        (_('Config Revision'), ('comment',))
+            name=_('Miscellaneous')
+        ),
+        FieldSet('comment', name=_('Config Revision'))
     )
     )
 
 
     class Meta:
     class Meta:

+ 58 - 55
netbox/dcim/forms/bulk_edit.py

@@ -13,6 +13,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
 from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
 from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.choices import WirelessRoleChoices
 from wireless.choices import WirelessRoleChoices
@@ -75,7 +76,7 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Region
     model = Region
     fieldsets = (
     fieldsets = (
-        (None, ('parent', 'description')),
+        FieldSet('parent', 'description'),
     )
     )
     nullable_fields = ('parent', 'description')
     nullable_fields = ('parent', 'description')
 
 
@@ -94,7 +95,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = SiteGroup
     model = SiteGroup
     fieldsets = (
     fieldsets = (
-        (None, ('parent', 'description')),
+        FieldSet('parent', 'description'),
     )
     )
     nullable_fields = ('parent', 'description')
     nullable_fields = ('parent', 'description')
 
 
@@ -154,7 +155,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Site
     model = Site
     fieldsets = (
     fieldsets = (
-        (None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
+        FieldSet('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
         'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
@@ -194,7 +195,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Location
     model = Location
     fieldsets = (
     fieldsets = (
-        (None, ('site', 'parent', 'status', 'tenant', 'description')),
+        FieldSet('site', 'parent', 'status', 'tenant', 'description'),
     )
     )
     nullable_fields = ('parent', 'tenant', 'description')
     nullable_fields = ('parent', 'tenant', 'description')
 
 
@@ -212,7 +213,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = RackRole
     model = RackRole
     fieldsets = (
     fieldsets = (
-        (None, ('color', 'description')),
+        FieldSet('color', 'description'),
     )
     )
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
@@ -341,12 +342,13 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Rack
     model = Rack
     fieldsets = (
     fieldsets = (
-        (_('Rack'), ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')),
-        (_('Location'), ('region', 'site_group', 'site', 'location')),
-        (_('Hardware'), (
+        FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
+        FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
+        FieldSet(
             'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
             'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
-        )),
-        (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
+            name=_('Hardware')
+        ),
+        FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
         'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
@@ -376,7 +378,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = RackReservation
     model = RackReservation
     fieldsets = (
     fieldsets = (
-        (None, ('user', 'tenant', 'description')),
+        FieldSet('user', 'tenant', 'description'),
     )
     )
     nullable_fields = ('comments',)
     nullable_fields = ('comments',)
 
 
@@ -390,7 +392,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Manufacturer
     model = Manufacturer
     fieldsets = (
     fieldsets = (
-        (None, ('description',)),
+        FieldSet('description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -450,11 +452,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = DeviceType
     model = DeviceType
     fieldsets = (
     fieldsets = (
-        (_('Device Type'), (
+        FieldSet(
             'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
             'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
-            'airflow', 'description',
-        )),
-        (_('Weight'), ('weight', 'weight_unit')),
+            'airflow', 'description', name=_('Device Type')
+        ),
+        FieldSet('weight', 'weight_unit', name=_('Weight')),
     )
     )
     nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
     nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
 
 
@@ -489,8 +491,8 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ModuleType
     model = ModuleType
     fieldsets = (
     fieldsets = (
-        (_('Module Type'), ('manufacturer', 'part_number', 'description')),
-        (_('Weight'), ('weight', 'weight_unit')),
+        FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')),
+        FieldSet('weight', 'weight_unit', name=_('Weight')),
     )
     )
     nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
     nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
 
 
@@ -518,7 +520,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = DeviceRole
     model = DeviceRole
     fieldsets = (
     fieldsets = (
-        (None, ('color', 'vm_role', 'config_template', 'description')),
+        FieldSet('color', 'vm_role', 'config_template', 'description'),
     )
     )
     nullable_fields = ('color', 'config_template', 'description')
     nullable_fields = ('color', 'config_template', 'description')
 
 
@@ -542,7 +544,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Platform
     model = Platform
     fieldsets = (
     fieldsets = (
-        (None, ('manufacturer', 'config_template', 'description')),
+        FieldSet('manufacturer', 'config_template', 'description'),
     )
     )
     nullable_fields = ('manufacturer', 'config_template', 'description')
     nullable_fields = ('manufacturer', 'config_template', 'description')
 
 
@@ -621,10 +623,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Device
     model = Device
     fieldsets = (
     fieldsets = (
-        (_('Device'), ('role', 'status', 'tenant', 'platform', 'description')),
-        (_('Location'), ('site', 'location')),
-        (_('Hardware'), ('manufacturer', 'device_type', 'airflow', 'serial')),
-        (_('Configuration'), ('config_template',)),
+        FieldSet('role', 'status', 'tenant', 'platform', 'description', name=_('Device')),
+        FieldSet('site', 'location', name=_('Location')),
+        FieldSet('manufacturer', 'device_type', 'airflow', 'serial', name=_('Hardware')),
+        FieldSet('config_template', name=_('Configuration')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
         'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
@@ -668,7 +670,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Module
     model = Module
     fieldsets = (
     fieldsets = (
-        (None, ('manufacturer', 'module_type', 'status', 'serial', 'description')),
+        FieldSet('manufacturer', 'module_type', 'status', 'serial', 'description'),
     )
     )
     nullable_fields = ('serial', 'description', 'comments')
     nullable_fields = ('serial', 'description', 'comments')
 
 
@@ -720,8 +722,8 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Cable
     model = Cable
     fieldsets = (
     fieldsets = (
-        (None, ('type', 'status', 'tenant', 'label', 'description')),
-        (_('Attributes'), ('color', 'length', 'length_unit')),
+        FieldSet('type', 'status', 'tenant', 'label', 'description'),
+        FieldSet('color', 'length', 'length_unit', name=_('Attributes')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
         'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
@@ -743,7 +745,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VirtualChassis
     model = VirtualChassis
     fieldsets = (
     fieldsets = (
-        (None, ('domain', 'description')),
+        FieldSet('domain', 'description'),
     )
     )
     nullable_fields = ('domain', 'description', 'comments')
     nullable_fields = ('domain', 'description', 'comments')
 
 
@@ -791,7 +793,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = PowerPanel
     model = PowerPanel
     fieldsets = (
     fieldsets = (
-        (None, ('region', 'site_group', 'site', 'location', 'description')),
+        FieldSet('region', 'site_group', 'site', 'location', 'description'),
     )
     )
     nullable_fields = ('location', 'description', 'comments')
     nullable_fields = ('location', 'description', 'comments')
 
 
@@ -861,8 +863,8 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     fieldsets = (
-        (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant')),
-        (_('Power'), ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
+        FieldSet('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant'),
+        FieldSet('supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Power'))
     )
     )
     nullable_fields = ('location', 'tenant', 'description', 'comments')
     nullable_fields = ('location', 'tenant', 'description', 'comments')
 
 
@@ -1210,7 +1212,7 @@ class ConsolePortBulkEditForm(
 
 
     model = ConsolePort
     model = ConsolePort
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
+        FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'),
     )
     )
     nullable_fields = ('module', 'label', 'description')
     nullable_fields = ('module', 'label', 'description')
 
 
@@ -1227,7 +1229,7 @@ class ConsoleServerPortBulkEditForm(
 
 
     model = ConsoleServerPort
     model = ConsoleServerPort
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
+        FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'),
     )
     )
     nullable_fields = ('module', 'label', 'description')
     nullable_fields = ('module', 'label', 'description')
 
 
@@ -1244,8 +1246,8 @@ class PowerPortBulkEditForm(
 
 
     model = PowerPort
     model = PowerPort
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'description', 'mark_connected')),
-        (_('Power'), ('maximum_draw', 'allocated_draw')),
+        FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
+        FieldSet('maximum_draw', 'allocated_draw', name=_('Power')),
     )
     )
     nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw')
     nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw')
 
 
@@ -1262,8 +1264,8 @@ class PowerOutletBulkEditForm(
 
 
     model = PowerOutlet
     model = PowerOutlet
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'description', 'mark_connected')),
-        (_('Power'), ('feed_leg', 'power_port')),
+        FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
+        FieldSet('feed_leg', 'power_port', name=_('Power')),
     )
     )
     nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
     nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
 
 
@@ -1395,20 +1397,21 @@ class InterfaceBulkEditForm(
 
 
     model = Interface
     model = Interface
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
-        (_('Addressing'), ('vrf', 'mac_address', 'wwn')),
-        (_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
-        (_('PoE'), ('poe_mode', 'poe_type')),
-        (_('Related Interfaces'), ('parent', 'bridge', 'lag')),
-        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
-        (_('Wireless'), (
+        FieldSet('module', 'type', 'label', 'speed', 'duplex', 'description'),
+        FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
+        FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
+        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
+        FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
+        FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
+        FieldSet(
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
-        )),
+            name=_('Wireless')
+        ),
     )
     )
     nullable_fields = (
     nullable_fields = (
-        'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
-        'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
-        'tagged_vlans', 'vrf', 'wireless_lans'
+        'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu',
+        'description', 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
+        'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
     )
     )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -1488,7 +1491,7 @@ class FrontPortBulkEditForm(
 
 
     model = FrontPort
     model = FrontPort
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
+        FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'),
     )
     )
     nullable_fields = ('module', 'label', 'description', 'color')
     nullable_fields = ('module', 'label', 'description', 'color')
 
 
@@ -1505,7 +1508,7 @@ class RearPortBulkEditForm(
 
 
     model = RearPort
     model = RearPort
     fieldsets = (
     fieldsets = (
-        (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
+        FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'),
     )
     )
     nullable_fields = ('module', 'label', 'description', 'color')
     nullable_fields = ('module', 'label', 'description', 'color')
 
 
@@ -1516,7 +1519,7 @@ class ModuleBayBulkEditForm(
 ):
 ):
     model = ModuleBay
     model = ModuleBay
     fieldsets = (
     fieldsets = (
-        (None, ('label', 'position', 'description')),
+        FieldSet('label', 'position', 'description'),
     )
     )
     nullable_fields = ('label', 'position', 'description')
     nullable_fields = ('label', 'position', 'description')
 
 
@@ -1527,7 +1530,7 @@ class DeviceBayBulkEditForm(
 ):
 ):
     model = DeviceBay
     model = DeviceBay
     fieldsets = (
     fieldsets = (
-        (None, ('label', 'description')),
+        FieldSet('label', 'description'),
     )
     )
     nullable_fields = ('label', 'description')
     nullable_fields = ('label', 'description')
 
 
@@ -1554,7 +1557,7 @@ class InventoryItemBulkEditForm(
 
 
     model = InventoryItem
     model = InventoryItem
     fieldsets = (
     fieldsets = (
-        (None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')),
+        FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'),
     )
     )
     nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
     nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
 
 
@@ -1576,7 +1579,7 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = InventoryItemRole
     model = InventoryItemRole
     fieldsets = (
     fieldsets = (
-        (None, ('color', 'description')),
+        FieldSet('color', 'description'),
     )
     )
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
@@ -1599,6 +1602,6 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
     model = VirtualDeviceContext
     model = VirtualDeviceContext
     fieldsets = (
     fieldsets = (
-        (None, ('device', 'status', 'tenant')),
+        FieldSet('device', 'status', 'tenant'),
     )
     )
     nullable_fields = ('device', 'tenant', )
     nullable_fields = ('device', 'tenant', )

+ 133 - 125
netbox/dcim/forms/filtersets.py

@@ -12,7 +12,8 @@ from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
-from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
+from utilities.forms.rendering import FieldSet
+from utilities.forms.widgets import NumberWithOptions
 from vpn.models import L2VPN
 from vpn.models import L2VPN
 from wireless.choices import *
 from wireless.choices import *
 
 
@@ -132,8 +133,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
 class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Region
     model = Region
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag', 'parent_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
+        FieldSet('q', 'filter_id', 'tag', 'parent_id'),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     parent_id = DynamicModelMultipleChoiceField(
     parent_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -146,8 +147,8 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = SiteGroup
     model = SiteGroup
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag', 'parent_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
+        FieldSet('q', 'filter_id', 'tag', 'parent_id'),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     parent_id = DynamicModelMultipleChoiceField(
     parent_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
@@ -160,10 +161,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
 class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Site
     model = Site
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('status', 'region_id', 'group_id', 'asn_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
     selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
@@ -192,10 +193,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
 class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
 class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Location
     model = Location
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -241,13 +242,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
 class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
 class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Rack
     model = Rack
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        (_('Function'), ('status', 'role_id')),
-        (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
-        (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
+        FieldSet('status', 'role_id', name=_('Function')),
+        FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
+        FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
     )
     )
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -326,13 +327,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
 
 
 class RackElevationFilterForm(RackFilterForm):
 class RackElevationFilterForm(RackFilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
-        (_('Function'), ('status', 'role_id')),
-        (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
-        (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
+        FieldSet('status', 'role_id', name=_('Function')),
+        FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
+        FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
     )
     )
     id = DynamicModelMultipleChoiceField(
     id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -348,10 +349,10 @@ class RackElevationFilterForm(RackFilterForm):
 class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = RackReservation
     model = RackReservation
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('User'), ('user_id',)),
-        (_('Rack'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('user_id', name=_('User')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -401,8 +402,8 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Manufacturer
     model = Manufacturer
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
@@ -410,14 +411,16 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
 class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
     model = DeviceType
     model = DeviceType
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Hardware'), ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
-        (_('Images'), ('has_front_image', 'has_rear_image')),
-        (_('Components'), (
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet(
+            'manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow', name=_('Hardware')
+        ),
+        FieldSet('has_front_image', 'has_rear_image', name=_('Images')),
+        FieldSet(
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
-            'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
-        )),
-        (_('Weight'), ('weight', 'weight_unit')),
+            'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
+        ),
+        FieldSet('weight', 'weight_unit', name=_('Weight')),
     )
     )
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
     manufacturer_id = DynamicModelMultipleChoiceField(
     manufacturer_id = DynamicModelMultipleChoiceField(
@@ -536,13 +539,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
 class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
 class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
     model = ModuleType
     model = ModuleType
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Hardware'), ('manufacturer_id', 'part_number')),
-        (_('Components'), (
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('manufacturer_id', 'part_number', name=_('Hardware')),
+        FieldSet(
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
-            'pass_through_ports',
-        )),
-        (_('Weight'), ('weight', 'weight_unit')),
+            'pass_through_ports', name=_('Components')
+        ),
+        FieldSet('weight', 'weight_unit', name=_('Weight')),
     )
     )
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
     manufacturer_id = DynamicModelMultipleChoiceField(
     manufacturer_id = DynamicModelMultipleChoiceField(
@@ -642,18 +645,20 @@ class DeviceFilterForm(
 ):
 ):
     model = Device
     model = Device
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
-        (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
-        (_('Components'), (
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
+        FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
+        FieldSet(
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
-        )),
-        (_('Miscellaneous'), (
+            name=_('Components')
+        ),
+        FieldSet(
             'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
             'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
-        ))
+            name=_('Miscellaneous')
+        )
     )
     )
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
     selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -817,9 +822,9 @@ class VirtualDeviceContextFilterForm(
 ):
 ):
     model = VirtualDeviceContext
     model = VirtualDeviceContext
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('device', 'status', 'has_primary_ip')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     device = DynamicModelMultipleChoiceField(
     device = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -844,8 +849,8 @@ class VirtualDeviceContextFilterForm(
 class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
 class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Module
     model = Module
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Hardware'), ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
     )
     )
     manufacturer_id = DynamicModelMultipleChoiceField(
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -879,9 +884,9 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
 class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VirtualChassis
     model = VirtualChassis
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -908,10 +913,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Cable
     model = Cable
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
-        (_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit', 'unterminated')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
+        FieldSet('type', 'status', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -992,9 +997,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = PowerPanel
     model = PowerPanel
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
     selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -1031,10 +1036,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
     )
     )
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -1141,11 +1146,11 @@ class PathEndpointFilterForm(CabledFilterForm):
 class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = ConsolePort
     model = ConsolePort
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type', 'speed')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Connection'), ('cabled', 'connected', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -1163,11 +1168,11 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = ConsoleServerPort
     model = ConsoleServerPort
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type', 'speed')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Connection'), ('cabled', 'connected', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -1185,11 +1190,11 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
 class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerPort
     model = PowerPort
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Connection'), ('cabled', 'connected', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -1202,11 +1207,11 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerOutlet
     model = PowerOutlet
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Connection'), ('cabled', 'connected', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -1219,14 +1224,14 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = Interface
     model = Interface
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
-        (_('Addressing'), ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
-        (_('PoE'), ('poe_mode', 'poe_type')),
-        (_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
-        (_('Connection'), ('cabled', 'connected', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
+        FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
+        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
+        FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
+        FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
     )
     )
     selector_fields = ('filter_id', 'q', 'device_id')
     selector_fields = ('filter_id', 'q', 'device_id')
     vdc_id = DynamicModelMultipleChoiceField(
     vdc_id = DynamicModelMultipleChoiceField(
@@ -1330,11 +1335,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 
 
 class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type', 'color')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Cable'), ('cabled', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'occupied', name=_('Cable')),
     )
     )
     model = FrontPort
     model = FrontPort
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
@@ -1352,11 +1357,11 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     model = RearPort
     model = RearPort
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'type', 'color')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        (_('Cable'), ('cabled', 'occupied')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+        FieldSet('cabled', 'occupied', name=_('Cable')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -1373,10 +1378,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class ModuleBayFilterForm(DeviceComponentFilterForm):
 class ModuleBayFilterForm(DeviceComponentFilterForm):
     model = ModuleBay
     model = ModuleBay
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'position')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', 'position', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
     position = forms.CharField(
     position = forms.CharField(
@@ -1388,10 +1393,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
     model = DeviceBay
     model = DeviceBay
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'label', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
@@ -1399,10 +1404,13 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
 class InventoryItemFilterForm(DeviceComponentFilterForm):
 class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
     model = InventoryItem
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet(
+            'name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
+            name=_('Attributes')
+        ),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
+        FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
     )
     )
     role_id = DynamicModelMultipleChoiceField(
     role_id = DynamicModelMultipleChoiceField(
         queryset=InventoryItemRole.objects.all(),
         queryset=InventoryItemRole.objects.all(),

+ 101 - 106
netbox/dcim/forms/model_forms.py

@@ -16,7 +16,7 @@ from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
     NumericArrayField, SlugField,
     NumericArrayField, SlugField,
 )
 )
-from utilities.forms.rendering import InlineFields, TabbedGroups
+from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
 from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.models import WirelessLAN, WirelessLANGroup
@@ -78,9 +78,7 @@ class RegionForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Region'), (
-            'parent', 'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('parent', 'name', 'slug', 'description', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -99,9 +97,7 @@ class SiteGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Site Group'), (
-            'parent', 'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('parent', 'name', 'slug', 'description', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -136,11 +132,12 @@ class SiteForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Site'), (
+        FieldSet(
             'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
             'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
-        )),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')),
+            name=_('Site')
+        ),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -180,8 +177,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -195,9 +192,7 @@ class RackRoleForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Rack Role'), (
-            'name', 'slug', 'color', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -229,19 +224,15 @@ class RackForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Rack'), ('site', 'location', 'name', 'status', 'role', 'description', 'tags')),
-        (_('Inventory Control'), ('facility_id', 'serial', 'asset_tag')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Dimensions'), (
-            'type',
-            'width',
-            'starting_unit',
-            'u_height',
+        FieldSet('site', 'location', 'name', 'status', 'role', 'description', 'tags', name=_('Rack')),
+        FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet(
+            'type', 'width', 'starting_unit', 'u_height',
             InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
             InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
             InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
             InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
-            'mounting_depth',
-            'desc_units',
-        )),
+            'mounting_depth', 'desc_units', name=_('Dimensions')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -273,8 +264,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('rack', 'units', 'user', 'description', 'tags', name=_('Reservation')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -288,9 +279,7 @@ class ManufacturerForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Manufacturer'), (
-            'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -321,12 +310,12 @@ class DeviceTypeForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
-        (_('Chassis'), (
+        FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
+        FieldSet(
             'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
             'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
-            'weight', 'weight_unit',
-        )),
-        (_('Images'), ('front_image', 'rear_image')),
+            'weight', 'weight_unit', name=_('Chassis')
+        ),
+        FieldSet('front_image', 'rear_image', name=_('Images')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -354,8 +343,8 @@ class ModuleTypeForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')),
-        (_('Weight'), ('weight', 'weight_unit'))
+        FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
+        FieldSet('weight', 'weight_unit', name=_('Weight'))
     )
     )
 
 
     class Meta:
     class Meta:
@@ -374,9 +363,9 @@ class DeviceRoleForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Device Role'), (
-            'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
-        )),
+        FieldSet(
+            'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', name=_('Device Role')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -403,7 +392,7 @@ class PlatformForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
+        FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -618,10 +607,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
-        (_('Hardware'), (
-            'serial', 'asset_tag', 'replicate_components', 'adopt_components',
-        )),
+        FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
+        FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -675,7 +662,7 @@ class PowerPanelForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
+        FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -700,9 +687,12 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
-        (_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet(
+            'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
+            name=_('Power Feed')
+        ),
+        FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -849,7 +839,7 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
 
 
 class ConsolePortTemplateForm(ModularComponentTemplateForm):
 class ConsolePortTemplateForm(ModularComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+        FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -861,7 +851,7 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
 
 
 class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
 class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+        FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -873,9 +863,9 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
 
 
 class PowerPortTemplateForm(ModularComponentTemplateForm):
 class PowerPortTemplateForm(ModularComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
             'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -896,7 +886,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
+        FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -918,9 +908,11 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
-        (_('PoE'), ('poe_mode', 'poe_type')),
-        (_('Wireless'), ('rf_role',)),
+        FieldSet(
+            'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
+        ),
+        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
+        FieldSet('rf_role', name=_('Wireless')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -942,10 +934,10 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
             'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
             'description',
             'description',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -958,7 +950,7 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
 
 
 class RearPortTemplateForm(ModularComponentTemplateForm):
 class RearPortTemplateForm(ModularComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
+        FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -970,7 +962,7 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
 
 
 class ModuleBayTemplateForm(ComponentTemplateForm):
 class ModuleBayTemplateForm(ComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'name', 'label', 'position', 'description')),
+        FieldSet('device_type', 'name', 'label', 'position', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -982,7 +974,7 @@ class ModuleBayTemplateForm(ComponentTemplateForm):
 
 
 class DeviceBayTemplateForm(ComponentTemplateForm):
 class DeviceBayTemplateForm(ComponentTemplateForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'name', 'label', 'description')),
+        FieldSet('device_type', 'name', 'label', 'description'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1023,10 +1015,10 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
             'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
             'component_type', 'component_id',
             'component_type', 'component_id',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1069,9 +1061,9 @@ class ModularDeviceComponentForm(DeviceComponentForm):
 
 
 class ConsolePortForm(ModularDeviceComponentForm):
 class ConsolePortForm(ModularDeviceComponentForm):
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1082,11 +1074,10 @@ class ConsolePortForm(ModularDeviceComponentForm):
 
 
 
 
 class ConsoleServerPortForm(ModularDeviceComponentForm):
 class ConsoleServerPortForm(ModularDeviceComponentForm):
-
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1097,12 +1088,11 @@ class ConsoleServerPortForm(ModularDeviceComponentForm):
 
 
 
 
 class PowerPortForm(ModularDeviceComponentForm):
 class PowerPortForm(ModularDeviceComponentForm):
-
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
             'description', 'tags',
             'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1124,10 +1114,10 @@ class PowerOutletForm(ModularDeviceComponentForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
             'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
             'tags',
             'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1223,15 +1213,18 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
-        (_('Addressing'), ('vrf', 'mac_address', 'wwn')),
-        (_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
-        (_('Related Interfaces'), ('parent', 'bridge', 'lag')),
-        (_('PoE'), ('poe_mode', 'poe_type')),
-        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
-        (_('Wireless'), (
+        FieldSet(
+            'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
+        ),
+        FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
+        FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
+        FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
+        FieldSet('poe_mode', 'poe_type', name=_('PoE')),
+        FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
+        FieldSet(
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
-        )),
+            name=_('Wireless')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1262,10 +1255,10 @@ class FrontPortForm(ModularDeviceComponentForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
             'description', 'tags',
             'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1278,9 +1271,9 @@ class FrontPortForm(ModularDeviceComponentForm):
 
 
 class RearPortForm(ModularDeviceComponentForm):
 class RearPortForm(ModularDeviceComponentForm):
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1292,7 +1285,7 @@ class RearPortForm(ModularDeviceComponentForm):
 
 
 class ModuleBayForm(DeviceComponentForm):
 class ModuleBayForm(DeviceComponentForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
+        FieldSet('device', 'name', 'label', 'position', 'description', 'tags',),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1304,7 +1297,7 @@ class ModuleBayForm(DeviceComponentForm):
 
 
 class DeviceBayForm(DeviceComponentForm):
 class DeviceBayForm(DeviceComponentForm):
     fieldsets = (
     fieldsets = (
-        (None, ('device', 'name', 'label', 'description', 'tags',)),
+        FieldSet('device', 'name', 'label', 'description', 'tags',),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1412,19 +1405,20 @@ class InventoryItemForm(DeviceComponentForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
-        (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
-        (_('Component Assignment'), (
+        FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')),
+        FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
+        FieldSet(
             TabbedGroups(
             TabbedGroups(
-                (_('Interface'), 'interface'),
-                (_('Console Port'), 'consoleport'),
-                (_('Console Server Port'), 'consoleserverport'),
-                (_('Front Port'), 'frontport'),
-                (_('Rear Port'), 'rearport'),
-                (_('Power Port'), 'powerport'),
-                (_('Power Outlet'), 'poweroutlet'),
+                FieldSet('interface', name=_('Interface')),
+                FieldSet('consoleport', name=_('Console Port')),
+                FieldSet('consoleserverport', name=_('Console Server Port')),
+                FieldSet('frontport', name=_('Front Port')),
+                FieldSet('rearport', name=_('Rear Port')),
+                FieldSet('powerport', name=_('Power Port')),
+                FieldSet('poweroutlet', name=_('Power Outlet')),
             ),
             ),
-        ))
+            name=_('Component Assignment')
+        )
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1484,9 +1478,7 @@ class InventoryItemRoleForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Inventory Item Role'), (
-            'name', 'slug', 'color', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -1522,8 +1514,11 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant'))
+        FieldSet(
+            'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
+            name=_('Virtual Device Context')
+        ),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
     )
     )
 
 
     class Meta:
     class Meta:

+ 4 - 3
netbox/dcim/forms/object_create.py

@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.models import *
 from dcim.models import *
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import APISelect
 from utilities.forms.widgets import APISelect
 from . import model_forms
 from . import model_forms
 
 
@@ -113,7 +114,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
 
 
     # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
     # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')),
+        FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
     )
     )
 
 
     class Meta(model_forms.FrontPortTemplateForm.Meta):
     class Meta(model_forms.FrontPortTemplateForm.Meta):
@@ -274,9 +275,9 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
 
 
     # Override fieldsets from FrontPortForm to omit rear_port_position
     # Override fieldsets from FrontPortForm to omit rear_port_position
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta(model_forms.FrontPortForm.Meta):
     class Meta(model_forms.FrontPortForm.Meta):

+ 35 - 34
netbox/extras/forms/filtersets.py

@@ -13,6 +13,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
     ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
 )
 )
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import APISelectMultiple, DateTimePicker
 from utilities.forms.widgets import APISelectMultiple, DateTimePicker
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
@@ -36,11 +37,11 @@ __all__ = (
 
 
 class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
 class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Attributes'), (
+        FieldSet('q', 'filter_id'),
+        FieldSet(
             'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
             'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
-            'ui_editable', 'is_cloneable',
-        )),
+            'ui_editable', 'is_cloneable', name=_('Attributes')
+        ),
     )
     )
     related_object_type_id = ContentTypeMultipleChoiceField(
     related_object_type_id = ContentTypeMultipleChoiceField(
         queryset=ObjectType.objects.with_feature('custom_fields'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
@@ -93,8 +94,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
 class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Choices'), ('base_choices', 'choice')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('base_choices', 'choice', name=_('Choices')),
     )
     )
     base_choices = forms.MultipleChoiceField(
     base_choices = forms.MultipleChoiceField(
         choices=CustomFieldChoiceSetBaseChoices,
         choices=CustomFieldChoiceSetBaseChoices,
@@ -107,8 +108,8 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
 class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('object_type', 'enabled', 'new_window', 'weight', name=_('Attributes')),
     )
     )
     object_type = ContentTypeMultipleChoiceField(
     object_type = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
@@ -137,9 +138,9 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
 class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Data'), ('data_source_id', 'data_file_id')),
-        (_('Attributes'), ('object_type_id', 'mime_type', 'file_extension', 'as_attachment')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('data_source_id', 'data_file_id', name=_('Data')),
+        FieldSet('object_type_id', 'mime_type', 'file_extension', 'as_attachment', name=_('Attributes')),
     )
     )
     data_source_id = DynamicModelMultipleChoiceField(
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
         queryset=DataSource.objects.all(),
@@ -178,8 +179,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
 class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Attributes'), ('object_type_id', 'name',)),
+        FieldSet('q', 'filter_id'),
+        FieldSet('object_type_id', 'name', name=_('Attributes')),
     )
     )
     object_type_id = ContentTypeChoiceField(
     object_type_id = ContentTypeChoiceField(
         label=_('Object type'),
         label=_('Object type'),
@@ -194,8 +195,8 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
 class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('object_type', 'enabled', 'shared', 'weight', name=_('Attributes')),
     )
     )
     object_type = ContentTypeMultipleChoiceField(
     object_type = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
@@ -225,8 +226,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
 class WebhookFilterForm(NetBoxModelFilterSetForm):
 class WebhookFilterForm(NetBoxModelFilterSetForm):
     model = Webhook
     model = Webhook
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('payload_url', 'http_method', 'http_content_type')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
     )
     )
     http_content_type = forms.CharField(
     http_content_type = forms.CharField(
         label=_('HTTP content type'),
         label=_('HTTP content type'),
@@ -249,9 +250,9 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('object_type_id', 'action_type', 'enabled')),
-        (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('object_type_id', 'action_type', 'enabled', name=_('Attributes')),
+        FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
     )
     )
     object_type_id = ContentTypeMultipleChoiceField(
     object_type_id = ContentTypeMultipleChoiceField(
         queryset=ObjectType.objects.with_feature('event_rules'),
         queryset=ObjectType.objects.with_feature('event_rules'),
@@ -323,12 +324,12 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag_id')),
-        (_('Data'), ('data_source_id', 'data_file_id')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        (_('Device'), ('device_type_id', 'platform_id', 'role_id')),
-        (_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id'))
+        FieldSet('q', 'filter_id', 'tag_id'),
+        FieldSet('data_source_id', 'data_file_id', name=_('Data')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
+        FieldSet('device_type_id', 'platform_id', 'role_id', name=_('Device')),
+        FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant'))
     )
     )
     data_source_id = DynamicModelMultipleChoiceField(
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
         queryset=DataSource.objects.all(),
@@ -412,8 +413,8 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
 
 
 class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Data'), ('data_source_id', 'data_file_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('data_source_id', 'data_file_id', name=_('Data')),
     )
     )
     data_source_id = DynamicModelMultipleChoiceField(
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
         queryset=DataSource.objects.all(),
@@ -444,9 +445,9 @@ class LocalConfigContextFilterForm(forms.Form):
 class JournalEntryFilterForm(NetBoxModelFilterSetForm):
 class JournalEntryFilterForm(NetBoxModelFilterSetForm):
     model = JournalEntry
     model = JournalEntry
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Creation'), ('created_before', 'created_after', 'created_by_id')),
-        (_('Attributes'), ('assigned_object_type_id', 'kind'))
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('created_before', 'created_after', 'created_by_id', name=_('Creation')),
+        FieldSet('assigned_object_type_id', 'kind', name=_('Attributes')),
     )
     )
     created_after = forms.DateTimeField(
     created_after = forms.DateTimeField(
         required=False,
         required=False,
@@ -482,9 +483,9 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
 class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
 class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
     model = ObjectChange
     model = ObjectChange
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id')),
-        (_('Time'), ('time_before', 'time_after')),
-        (_('Attributes'), ('action', 'user_id', 'changed_object_type_id')),
+        FieldSet('q', 'filter_id'),
+        FieldSet('time_before', 'time_after', name=_('Time')),
+        FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
     )
     )
     time_after = forms.DateTimeField(
     time_after = forms.DateTimeField(
         required=False,
         required=False,

+ 40 - 31
netbox/extras/forms/model_forms.py

@@ -17,7 +17,7 @@ from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
     CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
     DynamicModelMultipleChoiceField, JSONField, SlugField,
     DynamicModelMultipleChoiceField, JSONField, SlugField,
 )
 )
-from utilities.forms.rendering import ObjectAttribute
+from utilities.forms.rendering import FieldSet, ObjectAttribute
 from utilities.forms.widgets import ChoicesWidget, HTMXSelect
 from utilities.forms.widgets import ChoicesWidget, HTMXSelect
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
@@ -55,12 +55,15 @@ class CustomFieldForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Custom Field'), (
+        FieldSet(
             'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description',
             'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description',
-        )),
-        (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
-        (_('Values'), ('default', 'choice_set')),
-        (_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
+            name=_('Custom Field')
+        ),
+        FieldSet(
+            'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
+        ),
+        FieldSet('default', 'choice_set', name=_('Values')),
+        FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -129,8 +132,11 @@ class CustomLinkForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Custom Link'), ('name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
-        (_('Templates'), ('link_text', 'link_url')),
+        FieldSet(
+            'name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window',
+            name=_('Custom Link')
+        ),
+        FieldSet('link_text', 'link_url', name=_('Templates')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -163,9 +169,9 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Export Template'), ('name', 'object_types', 'description', 'template_code')),
-        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
-        (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
+        FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
+        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
+        FieldSet('mime_type', 'file_extension', 'as_attachment', name=_('Rendering')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -200,8 +206,8 @@ class SavedFilterForm(forms.ModelForm):
     parameters = JSONField()
     parameters = JSONField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')),
-        (_('Parameters'), ('parameters',)),
+        FieldSet('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', name=_('Saved Filter')),
+        FieldSet('parameters', name=_('Parameters')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -232,11 +238,12 @@ class BookmarkForm(forms.ModelForm):
 class WebhookForm(NetBoxModelForm):
 class WebhookForm(NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
-        (_('Webhook'), ('name', 'description', 'tags',)),
-        (_('HTTP Request'), (
+        FieldSet('name', 'description', 'tags', name=_('Webhook')),
+        FieldSet(
             'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
             'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
-        )),
-        (_('SSL'), ('ssl_verification', 'ca_file_path')),
+            name=_('HTTP Request')
+        ),
+        FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -267,12 +274,13 @@ class EventRuleForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Event Rule'), ('name', 'description', 'object_types', 'enabled', 'tags')),
-        (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
-        (_('Conditions'), ('conditions',)),
-        (_('Action'), (
+        FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
+        FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
+        FieldSet('conditions', name=_('Conditions')),
+        FieldSet(
             'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data',
             'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data',
-        )),
+            name=_('Action')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -361,7 +369,7 @@ class TagForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        ('Tag', ('name', 'slug', 'color', 'description', 'object_types')),
+        FieldSet('name', 'slug', 'color', 'description', 'object_types', name=_('Tag')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -443,12 +451,13 @@ class ConfigContextForm(SyncedDataMixin, forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Config Context'), ('name', 'weight', 'description', 'data', 'is_active')),
-        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
-        (_('Assignment'), (
+        FieldSet('name', 'weight', 'description', 'data', 'is_active', name=_('Config Context')),
+        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
+        FieldSet(
             'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
             'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
             'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
             'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
-        )),
+            name=_('Assignment')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -495,9 +504,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Config Template'), ('name', 'description', 'environment_params', 'tags')),
-        (_('Content'), ('template_code',)),
-        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
+        FieldSet('name', 'description', 'environment_params', 'tags', name=_('Config Template')),
+        FieldSet('template_code', name=_('Content')),
+        FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -528,7 +537,7 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
 
 
 class ImageAttachmentForm(forms.ModelForm):
 class ImageAttachmentForm(forms.ModelForm):
     fieldsets = (
     fieldsets = (
-        (None, (ObjectAttribute('parent'), 'name', 'image')),
+        FieldSet(ObjectAttribute('parent'), 'name', 'image'),
     )
     )
 
 
     class Meta:
     class Meta:

+ 23 - 20
netbox/ipam/forms/bulk_edit.py

@@ -13,6 +13,7 @@ from utilities.forms import add_blank_choice
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
 )
 )
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 
 
@@ -55,7 +56,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VRF
     model = VRF
     fieldsets = (
     fieldsets = (
-        (None, ('tenant', 'enforce_unique', 'description')),
+        FieldSet('tenant', 'enforce_unique', 'description'),
     )
     )
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
@@ -75,7 +76,7 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = RouteTarget
     model = RouteTarget
     fieldsets = (
     fieldsets = (
-        (None, ('tenant', 'description')),
+        FieldSet('tenant', 'description'),
     )
     )
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
@@ -94,7 +95,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = RIR
     model = RIR
     fieldsets = (
     fieldsets = (
-        (None, ('is_private', 'description')),
+        FieldSet('is_private', 'description'),
     )
     )
     nullable_fields = ('is_private', 'description')
     nullable_fields = ('is_private', 'description')
 
 
@@ -118,7 +119,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ASNRange
     model = ASNRange
     fieldsets = (
     fieldsets = (
-        (None, ('rir', 'tenant', 'description')),
+        FieldSet('rir', 'tenant', 'description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -148,7 +149,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ASN
     model = ASN
     fieldsets = (
     fieldsets = (
-        (None, ('sites', 'rir', 'tenant', 'description')),
+        FieldSet('sites', 'rir', 'tenant', 'description'),
     )
     )
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
@@ -177,7 +178,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Aggregate
     model = Aggregate
     fieldsets = (
     fieldsets = (
-        (None, ('rir', 'tenant', 'date_added', 'description')),
+        FieldSet('rir', 'tenant', 'date_added', 'description'),
     )
     )
     nullable_fields = ('date_added', 'description', 'comments')
     nullable_fields = ('date_added', 'description', 'comments')
 
 
@@ -195,7 +196,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Role
     model = Role
     fieldsets = (
     fieldsets = (
-        (None, ('weight', 'description')),
+        FieldSet('weight', 'description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -265,9 +266,9 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Prefix
     model = Prefix
     fieldsets = (
     fieldsets = (
-        (None, ('tenant', 'status', 'role', 'description')),
-        (_('Site'), ('region', 'site_group', 'site')),
-        (_('Addressing'), ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
+        FieldSet('tenant', 'status', 'role', 'description'),
+        FieldSet('region', 'site_group', 'site', name=_('Site')),
+        FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'site', 'vrf', 'tenant', 'role', 'description', 'comments',
         'site', 'vrf', 'tenant', 'role', 'description', 'comments',
@@ -309,7 +310,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IPRange
     model = IPRange
     fieldsets = (
     fieldsets = (
-        (None, ('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description')),
+        FieldSet('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'vrf', 'tenant', 'role', 'description', 'comments',
         'vrf', 'tenant', 'role', 'description', 'comments',
@@ -357,8 +358,8 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IPAddress
     model = IPAddress
     fieldsets = (
     fieldsets = (
-        (None, ('status', 'role', 'tenant', 'description')),
-        (_('Addressing'), ('vrf', 'mask_length', 'dns_name')),
+        FieldSet('status', 'role', 'tenant', 'description'),
+        FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
         'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
@@ -400,8 +401,8 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = FHRPGroup
     model = FHRPGroup
     fieldsets = (
     fieldsets = (
-        (None, ('protocol', 'group_id', 'name', 'description')),
-        (_('Authentication'), ('auth_type', 'auth_key')),
+        FieldSet('protocol', 'group_id', 'name', 'description'),
+        FieldSet('auth_type', 'auth_key', name=_('Authentication')),
     )
     )
     nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
     nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
 
 
@@ -485,8 +486,10 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VLANGroup
     model = VLANGroup
     fieldsets = (
     fieldsets = (
-        (None, ('site', 'min_vid', 'max_vid', 'description')),
-        (_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
+        FieldSet('site', 'min_vid', 'max_vid', 'description'),
+        FieldSet(
+            'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope')
+        ),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -556,8 +559,8 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VLAN
     model = VLAN
     fieldsets = (
     fieldsets = (
-        (None, ('status', 'role', 'tenant', 'description')),
-        (_('Site & Group'), ('region', 'site_group', 'site', 'group')),
+        FieldSet('status', 'role', 'tenant', 'description'),
+        FieldSet('region', 'site_group', 'site', 'group', name=_('Site & Group')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'site', 'group', 'tenant', 'role', 'description', 'comments',
         'site', 'group', 'tenant', 'role', 'description', 'comments',
@@ -587,7 +590,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ServiceTemplate
     model = ServiceTemplate
     fieldsets = (
     fieldsets = (
-        (None, ('protocol', 'ports', 'description')),
+        FieldSet('protocol', 'ports', 'description'),
     )
     )
     nullable_fields = ('description', 'comments')
     nullable_fields = ('description', 'comments')
 
 

+ 50 - 43
netbox/ipam/forms/filtersets.py

@@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.rendering import FieldSet
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from vpn.models import L2VPN
 from vpn.models import L2VPN
 
 
@@ -42,9 +43,9 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
 class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VRF
     model = VRF
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Route Targets'), ('import_target_id', 'export_target_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     import_target_id = DynamicModelMultipleChoiceField(
     import_target_id = DynamicModelMultipleChoiceField(
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
@@ -62,9 +63,9 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = RouteTarget
     model = RouteTarget
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('VRF'), ('importing_vrf_id', 'exporting_vrf_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     importing_vrf_id = DynamicModelMultipleChoiceField(
     importing_vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -94,9 +95,9 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
 class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Aggregate
     model = Aggregate
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('family', 'rir_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('family', 'rir_id', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     family = forms.ChoiceField(
     family = forms.ChoiceField(
         required=False,
         required=False,
@@ -114,9 +115,9 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = ASNRange
     model = ASNRange
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Range'), ('rir_id', 'start', 'end')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('rir_id', 'start', 'end', name=_('Range')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     rir_id = DynamicModelMultipleChoiceField(
     rir_id = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -137,9 +138,9 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = ASN
     model = ASN
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Assignment'), ('rir_id', 'site_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('rir_id', 'site_id', name=_('Assignment')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     rir_id = DynamicModelMultipleChoiceField(
     rir_id = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -162,11 +163,14 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
 class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Prefix
     model = Prefix
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Addressing'), ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
-        (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet(
+            'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
+            name=_('Addressing')
+        ),
+        FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     mask_length__lte = forms.IntegerField(
     mask_length__lte = forms.IntegerField(
         widget=forms.HiddenInput()
         widget=forms.HiddenInput()
@@ -251,9 +255,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = IPRange
     model = IPRange
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_utilized', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     family = forms.ChoiceField(
     family = forms.ChoiceField(
         required=False,
         required=False,
@@ -290,11 +294,14 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = IPAddress
     model = IPAddress
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name')),
-        (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Device/VM'), ('device_id', 'virtual_machine_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet(
+            'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
+            name=_('Attributes')
+        ),
+        FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
     )
     )
     selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
     selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
     parent = forms.CharField(
     parent = forms.CharField(
@@ -364,9 +371,9 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
 class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
     model = FHRPGroup
     model = FHRPGroup
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('name', 'protocol', 'group_id')),
-        (_('Authentication'), ('auth_type', 'auth_key')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
+        FieldSet('auth_type', 'auth_key', name=_('Authentication')),
     )
     )
     name = forms.CharField(
     name = forms.CharField(
         label=_('Name'),
         label=_('Name'),
@@ -396,9 +403,9 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
 
 
 class VLANGroupFilterForm(NetBoxModelFilterSetForm):
 class VLANGroupFilterForm(NetBoxModelFilterSetForm):
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region', 'sitegroup', 'site', 'location', 'rack')),
-        (_('VLAN ID'), ('min_vid', 'max_vid')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
+        FieldSet('min_vid', 'max_vid', name=_('VLAN ID')),
     )
     )
     model = VLANGroup
     model = VLANGroup
     region = DynamicModelMultipleChoiceField(
     region = DynamicModelMultipleChoiceField(
@@ -444,10 +451,10 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
 class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VLAN
     model = VLAN
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     selector_fields = ('filter_id', 'q', 'site_id')
     selector_fields = ('filter_id', 'q', 'site_id')
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -504,8 +511,8 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
 class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
     model = ServiceTemplate
     model = ServiceTemplate
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('protocol', 'port')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('protocol', 'port', name=_('Attributes')),
     )
     )
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
         label=_('Protocol'),
         label=_('Protocol'),
@@ -522,9 +529,9 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
 class ServiceFilterForm(ServiceTemplateFilterForm):
 class ServiceFilterForm(ServiceTemplateFilterForm):
     model = Service
     model = Service
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('protocol', 'port')),
-        (_('Assignment'), ('device_id', 'virtual_machine_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('protocol', 'port', name=_('Attributes')),
+        FieldSet('device_id', 'virtual_machine_id', name=_('Assignment')),
     )
     )
     device_id = DynamicModelMultipleChoiceField(
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),

+ 56 - 58
netbox/ipam/forms/model_forms.py

@@ -16,7 +16,7 @@ from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
     SlugField,
     SlugField,
 )
 )
-from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedGroups
+from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
 from utilities.forms.widgets import DatePicker
 from utilities.forms.widgets import DatePicker
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
 
 
@@ -57,9 +57,9 @@ class VRFForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('VRF'), ('name', 'rd', 'enforce_unique', 'description', 'tags')),
-        (_('Route Targets'), ('import_targets', 'export_targets')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
+        FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -75,8 +75,8 @@ class VRFForm(TenancyForm, NetBoxModelForm):
 
 
 class RouteTargetForm(TenancyForm, NetBoxModelForm):
 class RouteTargetForm(TenancyForm, NetBoxModelForm):
     fieldsets = (
     fieldsets = (
-        ('Route Target', ('name', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        FieldSet('name', 'description', 'tags', name=_('Route Target')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
     comments = CommentField()
     comments = CommentField()
 
 
@@ -91,9 +91,7 @@ class RIRForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('RIR'), (
-            'name', 'slug', 'is_private', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -111,8 +109,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Aggregate'), ('prefix', 'rir', 'date_added', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -132,8 +130,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
     )
     )
     slug = SlugField()
     slug = SlugField()
     fieldsets = (
     fieldsets = (
-        (_('ASN Range'), ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -156,8 +154,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('ASN'), ('asn', 'rir', 'sites', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -185,9 +183,7 @@ class RoleForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Role'), (
-            'name', 'slug', 'weight', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -227,9 +223,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Prefix'), ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
-        (_('Site/VLAN Assignment'), ('site', 'vlan')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet(
+            'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
+        ),
+        FieldSet('site', 'vlan', name=_('Site/VLAN Assignment')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -254,8 +252,11 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('IP Range'), ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet(
+            'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags',
+            name=_('IP Range')
+        ),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -309,17 +310,17 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Assignment'), (
+        FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet(
             TabbedGroups(
             TabbedGroups(
-                (_('Device'), 'interface'),
-                (_('Virtual Machine'), 'vminterface'),
-                (_('FHRP Group'), 'fhrpgroup'),
+                FieldSet('interface', name=_('Device')),
+                FieldSet('vminterface', name=_('Virtual Machine')),
+                FieldSet('fhrpgroup', name=_('FHRP Group')),
             ),
             ),
-            'primary_for_parent',
-        )),
-        (_('NAT IP (Inside)'), ('nat_inside',)),
+            'primary_for_parent', name=_('Assignment')
+        ),
+        FieldSet('nat_inside', name=_('NAT IP (Inside)')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -458,9 +459,9 @@ class FHRPGroupForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('FHRP Group'), ('protocol', 'group_id', 'name', 'description', 'tags')),
-        (_('Authentication'), ('auth_type', 'auth_key')),
-        (_('Virtual IP Address'), ('ip_vrf', 'ip_address', 'ip_status'))
+        FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
+        FieldSet('auth_type', 'auth_key', name=_('Authentication')),
+        FieldSet('ip_vrf', 'ip_address', 'ip_status', name=_('Virtual IP Address'))
     )
     )
 
 
     class Meta:
     class Meta:
@@ -518,7 +519,7 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (ObjectAttribute('interface'), 'group', 'priority')),
+        FieldSet(ObjectAttribute('interface'), 'group', 'priority'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -606,9 +607,12 @@ class VLANGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('VLAN Group'), ('name', 'slug', 'description', 'tags')),
-        (_('Child VLANs'), ('min_vid', 'max_vid')),
-        (_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
+        FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
+        FieldSet(
+            'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
+            name=_('Scope')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -681,9 +685,7 @@ class ServiceTemplateForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Service Template'), (
-            'name', 'protocol', 'ports', 'description', 'tags',
-        )),
+        FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Service Template')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -724,17 +726,15 @@ class ServiceForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Service'), (
+        FieldSet(
             TabbedGroups(
             TabbedGroups(
-                (_('Device'), 'device'),
-                (_('Virtual Machine'), 'virtual_machine'),
+                FieldSet('device', name=_('Device')),
+                FieldSet('virtual_machine', name=_('Virtual Machine')),
             ),
             ),
             'name',
             'name',
             InlineFields('protocol', 'ports', label=_('Port(s)')),
             InlineFields('protocol', 'ports', label=_('Port(s)')),
-            'ipaddresses',
-            'description',
-            'tags',
-        )),
+            'ipaddresses', 'description', 'tags', name=_('Service')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -752,19 +752,17 @@ class ServiceCreateForm(ServiceForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Service'), (
+        FieldSet(
             TabbedGroups(
             TabbedGroups(
-                (_('Device'), 'device'),
-                (_('Virtual Machine'), 'virtual_machine'),
+                FieldSet('device', name=_('Device')),
+                FieldSet('virtual_machine', name=_('Virtual Machine')),
             ),
             ),
             TabbedGroups(
             TabbedGroups(
-                (_('From Template'), 'service_template'),
-                (_('Custom'), 'name', 'protocol', 'ports'),
+                FieldSet('service_template', name=_('From Template')),
+                FieldSet('name', 'protocol', 'ports', name=_('Custom')),
             ),
             ),
-            'ipaddresses',
-            'description',
-            'tags',
-        )),
+            'ipaddresses', 'description', 'tags', name=_('Service')
+        ),
     )
     )
 
 
     class Meta(ServiceForm.Meta):
     class Meta(ServiceForm.Meta):

+ 1 - 1
netbox/netbox/forms/base.py

@@ -24,7 +24,7 @@ class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms
     Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
     Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
 
 
     Attributes:
     Attributes:
-        fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
+        fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
             the rendered form (optional). If not defined, the all fields will be rendered as a single section.
             the rendered form (optional). If not defined, the all fields will be rendered as a single section.
     """
     """
     fieldsets = ()
     fieldsets = ()

+ 7 - 3
netbox/templates/generic/bulk_edit.html

@@ -49,14 +49,18 @@ Context:
       {% if form.fieldsets %}
       {% if form.fieldsets %}
 
 
         {# Render grouped fields according to declared fieldsets #}
         {# Render grouped fields according to declared fieldsets #}
-        {% for group, fields in form.fieldsets %}
+        {% for fieldset in form.fieldsets %}
           <div class="field-group mb-5">
           <div class="field-group mb-5">
             <div class="row">
             <div class="row">
               <h5 class="col-9 offset-3">
               <h5 class="col-9 offset-3">
-                {% if group %}{{ group }}{% else %}{{ model|meta:"verbose_name"|bettertitle }}{% endif %}
+                {% if fieldset.name %}
+                  {{ fieldset.name }}
+                {% else %}
+                  {{ model|meta:"verbose_name"|bettertitle }}
+                {% endif %}
               </h5>
               </h5>
             </div>
             </div>
-            {% for name in fields %}
+            {% for name in fieldset.fields %}
               {% with field=form|getfield:name %}
               {% with field=form|getfield:name %}
                 {% if field.name in form.nullable_fields %}
                 {% if field.name in form.nullable_fields %}
                   {% render_field field bulk_nullable=True %}
                   {% render_field field bulk_nullable=True %}

+ 4 - 4
netbox/templates/inc/filter_list.html

@@ -9,14 +9,14 @@
         {{ field }}
         {{ field }}
       {% endfor %}
       {% endfor %}
       {# List filters by group #}
       {# List filters by group #}
-      {% for heading, fields in filter_form.fieldsets %}
+      {% for fieldset in filter_form.fieldsets %}
         <div class="col col-12">
         <div class="col col-12">
-          {% if heading %}
+          {% if fieldset.name %}
             <div class="hr-text">
             <div class="hr-text">
-              <span>{{ heading }}</span>
+              <span>{{ fieldset.name }}</span>
             </div>
             </div>
           {% endif %}
           {% endif %}
-          {% for name in fields %}
+          {% for name in fieldset.fields %}
             {% with field=filter_form|get_item:name %}
             {% with field=filter_form|get_item:name %}
               {% render_field field %}
               {% render_field field %}
             {% endwith %}
             {% endwith %}

+ 6 - 5
netbox/tenancy/forms/bulk_edit.py

@@ -6,6 +6,7 @@ from tenancy.choices import ContactPriorityChoices
 from tenancy.models import *
 from tenancy.models import *
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import CommentField, DynamicModelChoiceField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField
+from utilities.forms.rendering import FieldSet
 
 
 __all__ = (
 __all__ = (
     'ContactAssignmentBulkEditForm',
     'ContactAssignmentBulkEditForm',
@@ -46,7 +47,7 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Tenant
     model = Tenant
     fieldsets = (
     fieldsets = (
-        (None, ('group',)),
+        FieldSet('group'),
     )
     )
     nullable_fields = ('group',)
     nullable_fields = ('group',)
 
 
@@ -69,7 +70,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ContactGroup
     model = ContactGroup
     fieldsets = (
     fieldsets = (
-        (None, ('parent', 'description')),
+        FieldSet('parent', 'description'),
     )
     )
     nullable_fields = ('parent', 'description')
     nullable_fields = ('parent', 'description')
 
 
@@ -83,7 +84,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ContactRole
     model = ContactRole
     fieldsets = (
     fieldsets = (
-        (None, ('description',)),
+        FieldSet('description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -126,7 +127,7 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Contact
     model = Contact
     fieldsets = (
     fieldsets = (
-        (None, ('group', 'title', 'phone', 'email', 'address', 'link', 'description')),
+        FieldSet('group', 'title', 'phone', 'email', 'address', 'link', 'description'),
     )
     )
     nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments')
     nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments')
 
 
@@ -150,6 +151,6 @@ class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ContactAssignment
     model = ContactAssignment
     fieldsets = (
     fieldsets = (
-        (None, ('contact', 'role', 'priority')),
+        FieldSet('contact', 'role', 'priority'),
     )
     )
     nullable_fields = ('priority',)
     nullable_fields = ('priority',)

+ 5 - 4
netbox/tenancy/forms/filtersets.py

@@ -9,6 +9,7 @@ from tenancy.forms import ContactModelFilterForm
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
     ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
 )
 )
+from utilities.forms.rendering import FieldSet
 
 
 __all__ = (
 __all__ = (
     'ContactAssignmentFilterForm',
     'ContactAssignmentFilterForm',
@@ -37,8 +38,8 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm):
 class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
 class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Tenant
     model = Tenant
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag', 'group_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group'))
+        FieldSet('q', 'filter_id', 'tag', 'group_id'),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     group_id = DynamicModelMultipleChoiceField(
     group_id = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
@@ -82,8 +83,8 @@ class ContactFilterForm(NetBoxModelFilterSetForm):
 class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
 class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
     model = ContactAssignment
     model = ContactAssignment
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Assignment'), ('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority', name=_('Assignment')),
     )
     )
     object_type_id = ContentTypeMultipleChoiceField(
     object_type_id = ContentTypeMultipleChoiceField(
         queryset=ObjectType.objects.with_feature('contacts'),
         queryset=ObjectType.objects.with_feature('contacts'),

+ 10 - 13
netbox/tenancy/forms/model_forms.py

@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.models import *
 from tenancy.models import *
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
-from utilities.forms.rendering import ObjectAttribute
+from utilities.forms.rendering import FieldSet, ObjectAttribute
 
 
 __all__ = (
 __all__ = (
     'ContactAssignmentForm',
     'ContactAssignmentForm',
@@ -29,9 +29,7 @@ class TenantGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Tenant Group'), (
-            'parent', 'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -51,7 +49,7 @@ class TenantForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Tenant'), ('name', 'slug', 'group', 'description', 'tags')),
+        FieldSet('name', 'slug', 'group', 'description', 'tags', name=_('Tenant')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -74,9 +72,7 @@ class ContactGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Contact Group'), (
-            'parent', 'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -88,9 +84,7 @@ class ContactRoleForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Contact Role'), (
-            'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('Contact Role')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -107,7 +101,10 @@ class ContactForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Contact'), ('group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags')),
+        FieldSet(
+            'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags',
+            name=_('Contact')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -142,7 +139,7 @@ class ContactAssignmentForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags')),
+        FieldSet(ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:

+ 4 - 3
netbox/users/forms/bulk_edit.py

@@ -6,6 +6,7 @@ from ipam.formfields import IPNetworkFormField
 from ipam.validators import prefix_validator
 from ipam.validators import prefix_validator
 from users.models import *
 from users.models import *
 from utilities.forms import BulkEditForm
 from utilities.forms import BulkEditForm
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker
 from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker
 
 
 __all__ = (
 __all__ = (
@@ -48,7 +49,7 @@ class UserBulkEditForm(forms.Form):
 
 
     model = User
     model = User
     fieldsets = (
     fieldsets = (
-        (None, ('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser')),
+        FieldSet('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser'),
     )
     )
     nullable_fields = ('first_name', 'last_name')
     nullable_fields = ('first_name', 'last_name')
 
 
@@ -71,7 +72,7 @@ class ObjectPermissionBulkEditForm(forms.Form):
 
 
     model = ObjectPermission
     model = ObjectPermission
     fieldsets = (
     fieldsets = (
-        (None, ('enabled', 'description')),
+        FieldSet('enabled', 'description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -104,7 +105,7 @@ class TokenBulkEditForm(BulkEditForm):
 
 
     model = Token
     model = Token
     fieldsets = (
     fieldsets = (
-        (None, ('write_enabled', 'description', 'expires', 'allowed_ips')),
+        FieldSet('write_enabled', 'description', 'expires', 'allowed_ips'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'expires', 'description', 'allowed_ips',
         'expires', 'description', 'allowed_ips',

+ 10 - 9
netbox/users/forms/filtersets.py

@@ -7,6 +7,7 @@ from netbox.forms.mixins import SavedFiltersMixin
 from users.models import Group, ObjectPermission, Token, User
 from users.models import Group, ObjectPermission, Token, User
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms.fields import DynamicModelMultipleChoiceField
 from utilities.forms.fields import DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DateTimePicker
 from utilities.forms.widgets import DateTimePicker
 
 
 __all__ = (
 __all__ = (
@@ -20,16 +21,16 @@ __all__ = (
 class GroupFilterForm(NetBoxModelFilterSetForm):
 class GroupFilterForm(NetBoxModelFilterSetForm):
     model = Group
     model = Group
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id',)),
+        FieldSet('q', 'filter_id',),
     )
     )
 
 
 
 
 class UserFilterForm(NetBoxModelFilterSetForm):
 class UserFilterForm(NetBoxModelFilterSetForm):
     model = User
     model = User
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id',)),
-        (_('Group'), ('group_id',)),
-        (_('Status'), ('is_active', 'is_staff', 'is_superuser')),
+        FieldSet('q', 'filter_id',),
+        FieldSet('group_id', name=_('Group')),
+        FieldSet('is_active', 'is_staff', 'is_superuser', name=_('Status')),
     )
     )
     group_id = DynamicModelMultipleChoiceField(
     group_id = DynamicModelMultipleChoiceField(
         queryset=Group.objects.all(),
         queryset=Group.objects.all(),
@@ -62,9 +63,9 @@ class UserFilterForm(NetBoxModelFilterSetForm):
 class ObjectPermissionFilterForm(NetBoxModelFilterSetForm):
 class ObjectPermissionFilterForm(NetBoxModelFilterSetForm):
     model = ObjectPermission
     model = ObjectPermission
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id',)),
-        (_('Permission'), ('enabled', 'group_id', 'user_id')),
-        (_('Actions'), ('can_view', 'can_add', 'can_change', 'can_delete')),
+        FieldSet('q', 'filter_id',),
+        FieldSet('enabled', 'group_id', 'user_id', name=_('Permission')),
+        FieldSet('can_view', 'can_add', 'can_change', 'can_delete', name=_('Actions')),
     )
     )
     enabled = forms.NullBooleanField(
     enabled = forms.NullBooleanField(
         label=_('Enabled'),
         label=_('Enabled'),
@@ -116,8 +117,8 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm):
 class TokenFilterForm(SavedFiltersMixin, FilterForm):
 class TokenFilterForm(SavedFiltersMixin, FilterForm):
     model = Token
     model = Token
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id',)),
-        (_('Token'), ('user_id', 'write_enabled', 'expires', 'last_used')),
+        FieldSet('q', 'filter_id',),
+        FieldSet('user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')),
     )
     )
     user_id = DynamicModelMultipleChoiceField(
     user_id = DynamicModelMultipleChoiceField(
         queryset=get_user_model().objects.all(),
         queryset=get_user_model().objects.all(),

+ 17 - 21
netbox/users/forms/model_forms.py

@@ -13,6 +13,7 @@ from netbox.preferences import PREFERENCES
 from users.constants import *
 from users.constants import *
 from users.models import *
 from users.models import *
 from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DateTimePicker
 from utilities.forms.widgets import DateTimePicker
 from utilities.permissions import qs_filter_from_constraints
 from utilities.permissions import qs_filter_from_constraints
 from utilities.utils import flatten_dict
 from utilities.utils import flatten_dict
@@ -53,15 +54,10 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
 
 
 class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
 class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
     fieldsets = (
     fieldsets = (
-        (_('User Interface'), (
-            'locale.language',
-            'pagination.per_page',
-            'pagination.placement',
-            'ui.colormode',
-        )),
-        (_('Miscellaneous'), (
-            'data_format',
-        )),
+        FieldSet(
+            'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', name=_('User Interface')
+        ),
+        FieldSet('data_format', name=_('Miscellaneous')),
     )
     )
     # List of clearable preferences
     # List of clearable preferences
     pk = forms.MultipleChoiceField(
     pk = forms.MultipleChoiceField(
@@ -189,10 +185,10 @@ class UserForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('User'), ('username', 'password', 'confirm_password', 'first_name', 'last_name', 'email')),
-        (_('Groups'), ('groups', )),
-        (_('Status'), ('is_active', 'is_staff', 'is_superuser')),
-        (_('Permissions'), ('object_permissions',)),
+        FieldSet('username', 'password', 'confirm_password', 'first_name', 'last_name', 'email', name=_('User')),
+        FieldSet('groups', name=_('Groups')),
+        FieldSet('is_active', 'is_staff', 'is_superuser', name=_('Status')),
+        FieldSet('object_permissions', name=_('Permissions')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -246,9 +242,9 @@ class GroupForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('name', )),
-        (_('Users'), ('users', )),
-        (_('Permissions'), ('object_permissions', )),
+        FieldSet('name'),
+        FieldSet('users', name=_('Users')),
+        FieldSet('object_permissions', name=_('Permissions')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -312,11 +308,11 @@ class ObjectPermissionForm(forms.ModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('name', 'description', 'enabled',)),
-        (_('Actions'), ('can_view', 'can_add', 'can_change', 'can_delete', 'actions')),
-        (_('Objects'), ('object_types', )),
-        (_('Assignment'), ('groups', 'users')),
-        (_('Constraints'), ('constraints',))
+        FieldSet('name', 'description', 'enabled'),
+        FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
+        FieldSet('object_types', name=_('Objects')),
+        FieldSet('groups', 'users', name=_('Assignment')),
+        FieldSet('constraints', name=_('Constraints'))
     )
     )
 
 
     class Meta:
     class Meta:

+ 5 - 4
netbox/utilities/forms/rendering.py

@@ -33,10 +33,11 @@ class TabbedGroups:
     """
     """
     Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate.
     Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate.
     """
     """
-    def __init__(self, *groups):
-        self.groups = [
-            FieldSet(*group, name=name) for name, *group in groups
-        ]
+    def __init__(self, *fieldsets):
+        for fs in fieldsets:
+            if not fs.name:
+                raise ValueError(f"Grouped fieldset {fs} must have a name.")
+        self.groups = fieldsets
 
 
         # Initialize a random ID for the group (for tab selection)
         # Initialize a random ID for the group (for tab selection)
         self.id = ''.join(
         self.id = ''.join(

+ 6 - 0
netbox/utilities/templatetags/form_helpers.py

@@ -1,3 +1,5 @@
+import warnings
+
 from django import template
 from django import template
 
 
 from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
 from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
@@ -52,8 +54,12 @@ def render_fieldset(form, fieldset):
     """
     """
     Render a group set of fields.
     Render a group set of fields.
     """
     """
+    # TODO: Remove in NetBox v4.1
     # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3'))
     # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3'))
     if type(fieldset) is not FieldSet:
     if type(fieldset) is not FieldSet:
+        warnings.warn(
+            f"{form.__class__} fieldsets contains a non-FieldSet item: {fieldset}"
+        )
         name, fields = fieldset
         name, fields = fieldset
         fieldset = FieldSet(*fields, name=name)
         fieldset = FieldSet(*fields, name=name)
 
 

+ 12 - 11
netbox/virtualization/forms/bulk_edit.py

@@ -10,6 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import BulkRenameForm, add_blank_choice
 from utilities.forms import BulkRenameForm, add_blank_choice
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from virtualization.choices import *
 from virtualization.choices import *
 from virtualization.models import *
 from virtualization.models import *
@@ -35,7 +36,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ClusterType
     model = ClusterType
     fieldsets = (
     fieldsets = (
-        (None, ('description',)),
+        FieldSet('description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -49,7 +50,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = ClusterGroup
     model = ClusterGroup
     fieldsets = (
     fieldsets = (
-        (None, ('description',)),
+        FieldSet('description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
@@ -104,8 +105,8 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Cluster
     model = Cluster
     fieldsets = (
     fieldsets = (
-        (None, ('type', 'group', 'status', 'tenant', 'description')),
-        (_('Site'), ('region', 'site_group', 'site')),
+        FieldSet('type', 'group', 'status', 'tenant', 'description'),
+        FieldSet('region', 'site_group', 'site', name=_('Site')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'group', 'site', 'tenant', 'description', 'comments',
         'group', 'site', 'tenant', 'description', 'comments',
@@ -185,9 +186,9 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VirtualMachine
     model = VirtualMachine
     fieldsets = (
     fieldsets = (
-        (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),
-        (_('Resources'), ('vcpus', 'memory', 'disk')),
-        ('Configuration', ('config_template',)),
+        FieldSet('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description'),
+        FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
+        FieldSet('config_template', name=_('Configuration')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
@@ -262,9 +263,9 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VMInterface
     model = VMInterface
     fieldsets = (
     fieldsets = (
-        (None, ('mtu', 'enabled', 'vrf', 'description')),
-        (_('Related Interfaces'), ('parent', 'bridge')),
-        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
+        FieldSet('mtu', 'enabled', 'vrf', 'description'),
+        FieldSet('parent', 'bridge', name=_('Related Interfaces')),
+        FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'parent', 'bridge', 'mtu', 'vrf', 'description',
         'parent', 'bridge', 'mtu', 'vrf', 'description',
@@ -340,7 +341,7 @@ class VirtualDiskBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = VirtualDisk
     model = VirtualDisk
     fieldsets = (
     fieldsets = (
-        (None, ('size', 'description')),
+        FieldSet('size', 'description'),
     )
     )
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 

+ 23 - 19
netbox/virtualization/forms/filtersets.py

@@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.rendering import FieldSet
 from virtualization.choices import *
 from virtualization.choices import *
 from virtualization.models import *
 from virtualization.models import *
 from vpn.models import L2VPN
 from vpn.models import L2VPN
@@ -32,19 +33,19 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = ClusterGroup
     model = ClusterGroup
     tag = TagFilterField(model)
     tag = TagFilterField(model)
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
 
 
 
 
 class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
 class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Cluster
     model = Cluster
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('group_id', 'type_id', 'status')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('group_id', 'type_id', 'status', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     selector_fields = ('filter_id', 'q', 'group_id')
     selector_fields = ('filter_id', 'q', 'group_id')
     type_id = DynamicModelMultipleChoiceField(
     type_id = DynamicModelMultipleChoiceField(
@@ -94,12 +95,15 @@ class VirtualMachineFilterForm(
 ):
 ):
     model = VirtualMachine
     model = VirtualMachine
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
-        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
-        (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')),
+        FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+        FieldSet(
+            'status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id',
+            'local_context_data', name=_('Attributes')
+        ),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
     cluster_group_id = DynamicModelMultipleChoiceField(
     cluster_group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
@@ -185,9 +189,9 @@ class VirtualMachineFilterForm(
 class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
 class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
     model = VMInterface
     model = VMInterface
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')),
-        (_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')),
+        FieldSet('enabled', 'mac_address', 'vrf_id', 'l2vpn_id', name=_('Attributes')),
     )
     )
     selector_fields = ('filter_id', 'q', 'virtual_machine_id')
     selector_fields = ('filter_id', 'q', 'virtual_machine_id')
     cluster_id = DynamicModelMultipleChoiceField(
     cluster_id = DynamicModelMultipleChoiceField(
@@ -230,9 +234,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
 class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
 class VirtualDiskFilterForm(NetBoxModelFilterSetForm):
     model = VirtualDisk
     model = VirtualDisk
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Virtual Machine'), ('virtual_machine_id',)),
-        (_('Attributes'), ('size',)),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('virtual_machine_id', name=_('Virtual Machine')),
+        FieldSet('size', name=_('Attributes')),
     )
     )
     virtual_machine_id = DynamicModelMultipleChoiceField(
     virtual_machine_id = DynamicModelMultipleChoiceField(
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),

+ 17 - 20
netbox/virtualization/forms/model_forms.py

@@ -13,6 +13,7 @@ from utilities.forms import ConfirmationForm
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField,
     CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField,
 )
 )
+from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import HTMXSelect
 from utilities.forms.widgets import HTMXSelect
 from virtualization.models import *
 from virtualization.models import *
 
 
@@ -32,9 +33,7 @@ class ClusterTypeForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Cluster Type'), (
-            'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Type')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -48,9 +47,7 @@ class ClusterGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Cluster Group'), (
-            'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Group')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -79,8 +76,8 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Cluster'), ('name', 'type', 'group', 'site', 'status', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('name', 'type', 'group', 'site', 'status', 'description', 'tags', name=_('Cluster')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -220,12 +217,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Virtual Machine'), ('name', 'role', 'status', 'description', 'tags')),
-        (_('Site/Cluster'), ('site', 'cluster', 'device')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Management'), ('platform', 'primary_ip4', 'primary_ip6', 'config_template')),
-        (_('Resources'), ('vcpus', 'memory', 'disk')),
-        (_('Config Context'), ('local_context_data',)),
+        FieldSet('name', 'role', 'status', 'description', 'tags', name=_('Virtual Machine')),
+        FieldSet('site', 'cluster', 'device', name=_('Site/Cluster')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')),
+        FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
+        FieldSet('local_context_data', name=_('Config Context')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -348,11 +345,11 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Interface'), ('virtual_machine', 'name', 'description', 'tags')),
-        (_('Addressing'), ('vrf', 'mac_address')),
-        (_('Operation'), ('mtu', 'enabled')),
-        (_('Related Interfaces'), ('parent', 'bridge')),
-        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
+        FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
+        FieldSet('vrf', 'mac_address', name=_('Addressing')),
+        FieldSet('mtu', 'enabled', name=_('Operation')),
+        FieldSet('parent', 'bridge', name=_('Related Interfaces')),
+        FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -372,7 +369,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
 class VirtualDiskForm(VMComponentForm):
 class VirtualDiskForm(VMComponentForm):
 
 
     fieldsets = (
     fieldsets = (
-        (_('Disk'), ('virtual_machine', 'name', 'size', 'description', 'tags')),
+        FieldSet('virtual_machine', 'name', 'size', 'description', 'tags', name=_('Disk')),
     )
     )
 
 
     class Meta:
     class Meta:

+ 12 - 15
netbox/vpn/forms/bulk_edit.py

@@ -5,6 +5,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.rendering import FieldSet
 from vpn.choices import *
 from vpn.choices import *
 from vpn.models import *
 from vpn.models import *
 
 
@@ -72,9 +73,9 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Tunnel
     model = Tunnel
     fieldsets = (
     fieldsets = (
-        (_('Tunnel'), ('status', 'group', 'encapsulation', 'tunnel_id', 'description')),
-        (_('Security'), ('ipsec_profile',)),
-        (_('Tenancy'), ('tenant',)),
+        FieldSet('status', 'group', 'encapsulation', 'tunnel_id', 'description', name=_('Tunnel')),
+        FieldSet('ipsec_profile', name=_('Security')),
+        FieldSet('tenant', name=_('Tenancy')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
         'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments',
@@ -125,10 +126,10 @@ class IKEProposalBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IKEProposal
     model = IKEProposal
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
             'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
             'description',
             'description',
-        )),
+        ),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'sa_lifetime', 'description', 'comments',
         'sa_lifetime', 'description', 'comments',
@@ -159,9 +160,7 @@ class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IKEPolicy
     model = IKEPolicy
     fieldsets = (
     fieldsets = (
-        (None, (
-            'version', 'mode', 'preshared_key', 'description',
-        )),
+        FieldSet('version', 'mode', 'preshared_key', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'mode', 'preshared_key', 'description', 'comments',
         'mode', 'preshared_key', 'description', 'comments',
@@ -196,10 +195,10 @@ class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IPSecProposal
     model = IPSecProposal
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
             'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
             'description',
             'description',
-        )),
+        ),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments',
         'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments',
@@ -221,7 +220,7 @@ class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IPSecPolicy
     model = IPSecPolicy
     fieldsets = (
     fieldsets = (
-        (None, ('pfs_group', 'description',)),
+        FieldSet('pfs_group', 'description'),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'pfs_group', 'description', 'comments',
         'pfs_group', 'description', 'comments',
@@ -253,9 +252,7 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = IPSecProfile
     model = IPSecProfile
     fieldsets = (
     fieldsets = (
-        (_('Profile'), (
-            'mode', 'ike_policy', 'ipsec_policy', 'description',
-        )),
+        FieldSet('mode', 'ike_policy', 'ipsec_policy', 'description', name=_('Profile')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'description', 'comments',
         'description', 'comments',
@@ -282,7 +279,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = L2VPN
     model = L2VPN
     fieldsets = (
     fieldsets = (
-        (None, ('type', 'tenant', 'description')),
+        FieldSet('type', 'tenant', 'description'),
     )
     )
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 

+ 26 - 22
netbox/vpn/forms/filtersets.py

@@ -9,6 +9,7 @@ from tenancy.forms import TenancyFilterForm
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
     ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
 )
 )
+from utilities.forms.rendering import FieldSet
 from utilities.forms.utils import add_blank_choice
 from utilities.forms.utils import add_blank_choice
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from vpn.choices import *
 from vpn.choices import *
@@ -37,10 +38,10 @@ class TunnelGroupFilterForm(NetBoxModelFilterSetForm):
 class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Tunnel
     model = Tunnel
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')),
-        (_('Security'), ('ipsec_profile_id',)),
-        (_('Tenancy'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')),
+        FieldSet('ipsec_profile_id', name=_('Security')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')),
     )
     )
     status = forms.MultipleChoiceField(
     status = forms.MultipleChoiceField(
         label=_('Status'),
         label=_('Status'),
@@ -72,8 +73,8 @@ class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
 class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
     model = TunnelTermination
     model = TunnelTermination
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Termination'), ('tunnel_id', 'role')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('tunnel_id', 'role', name=_('Termination')),
     )
     )
     tunnel_id = DynamicModelMultipleChoiceField(
     tunnel_id = DynamicModelMultipleChoiceField(
         queryset=Tunnel.objects.all(),
         queryset=Tunnel.objects.all(),
@@ -91,8 +92,10 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm):
 class IKEProposalFilterForm(NetBoxModelFilterSetForm):
 class IKEProposalFilterForm(NetBoxModelFilterSetForm):
     model = IKEProposal
     model = IKEProposal
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Parameters'), ('authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet(
+            'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters')
+        ),
     )
     )
     authentication_method = forms.MultipleChoiceField(
     authentication_method = forms.MultipleChoiceField(
         label=_('Authentication method'),
         label=_('Authentication method'),
@@ -120,8 +123,8 @@ class IKEProposalFilterForm(NetBoxModelFilterSetForm):
 class IKEPolicyFilterForm(NetBoxModelFilterSetForm):
 class IKEPolicyFilterForm(NetBoxModelFilterSetForm):
     model = IKEPolicy
     model = IKEPolicy
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Parameters'), ('version', 'mode', 'proposal_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')),
     )
     )
     version = forms.MultipleChoiceField(
     version = forms.MultipleChoiceField(
         label=_('IKE version'),
         label=_('IKE version'),
@@ -144,8 +147,8 @@ class IKEPolicyFilterForm(NetBoxModelFilterSetForm):
 class IPSecProposalFilterForm(NetBoxModelFilterSetForm):
 class IPSecProposalFilterForm(NetBoxModelFilterSetForm):
     model = IPSecProposal
     model = IPSecProposal
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Parameters'), ('encryption_algorithm', 'authentication_algorithm')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')),
     )
     )
     encryption_algorithm = forms.MultipleChoiceField(
     encryption_algorithm = forms.MultipleChoiceField(
         label=_('Encryption algorithm'),
         label=_('Encryption algorithm'),
@@ -163,8 +166,8 @@ class IPSecProposalFilterForm(NetBoxModelFilterSetForm):
 class IPSecPolicyFilterForm(NetBoxModelFilterSetForm):
 class IPSecPolicyFilterForm(NetBoxModelFilterSetForm):
     model = IPSecPolicy
     model = IPSecPolicy
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Parameters'), ('proposal_id', 'pfs_group')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('proposal_id', 'pfs_group', name=_('Parameters')),
     )
     )
     proposal_id = DynamicModelMultipleChoiceField(
     proposal_id = DynamicModelMultipleChoiceField(
         queryset=IKEProposal.objects.all(),
         queryset=IKEProposal.objects.all(),
@@ -182,8 +185,8 @@ class IPSecPolicyFilterForm(NetBoxModelFilterSetForm):
 class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
 class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
     model = IPSecProfile
     model = IPSecProfile
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Profile'), ('mode', 'ike_policy_id', 'ipsec_policy_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')),
     )
     )
     mode = forms.MultipleChoiceField(
     mode = forms.MultipleChoiceField(
         label=_('Mode'),
         label=_('Mode'),
@@ -206,9 +209,9 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
 class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = L2VPN
     model = L2VPN
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('type', 'import_target_id', 'export_target_id', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         label=_('Type'),
         label=_('Type'),
@@ -231,10 +234,11 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
 class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
     model = L2VPNTermination
     model = L2VPNTermination
     fieldsets = (
     fieldsets = (
-        (None, ('filter_id', 'l2vpn_id',)),
-        (_('Assigned Object'), (
+        FieldSet('filter_id', 'l2vpn_id',),
+        FieldSet(
             'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
             'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
-        )),
+            name=_('Assigned Object')
+        ),
     )
     )
     l2vpn_id = DynamicModelChoiceField(
     l2vpn_id = DynamicModelChoiceField(
         queryset=L2VPN.objects.all(),
         queryset=L2VPN.objects.all(),

+ 35 - 35
netbox/vpn/forms/model_forms.py

@@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
-from utilities.forms.rendering import TabbedGroups
+from utilities.forms.rendering import FieldSet, TabbedGroups
 from utilities.forms.utils import add_blank_choice, get_field_value
 from utilities.forms.utils import add_blank_choice, get_field_value
 from utilities.forms.widgets import HTMXSelect
 from utilities.forms.widgets import HTMXSelect
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
@@ -33,7 +33,7 @@ class TunnelGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Tunnel Group'), ('name', 'slug', 'description', 'tags')),
+        FieldSet('name', 'slug', 'description', 'tags', name=_('Tunnel Group')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -57,9 +57,9 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Tunnel'), ('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags')),
-        (_('Security'), ('ipsec_profile',)),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
+        FieldSet('ipsec_profile', name=_('Security')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -142,17 +142,15 @@ class TunnelCreateForm(TunnelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Tunnel'), ('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags')),
-        (_('Security'), ('ipsec_profile',)),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('First Termination'), (
+        FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')),
+        FieldSet('ipsec_profile', name=_('Security')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet(
             'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
             'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination',
-            'termination1_outside_ip',
-        )),
-        (_('Second Termination'), (
+            'termination1_outside_ip', name=_('First Termination')),
+        FieldSet(
             'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination',
             'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination',
-            'termination2_outside_ip',
-        )),
+            'termination2_outside_ip', name=_('Second Termination')),
     )
     )
 
 
     def __init__(self, *args, initial=None, **kwargs):
     def __init__(self, *args, initial=None, **kwargs):
@@ -254,7 +252,7 @@ class TunnelTerminationForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, ('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags')),
+        FieldSet('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -297,10 +295,11 @@ class TunnelTerminationForm(NetBoxModelForm):
 class IKEProposalForm(NetBoxModelForm):
 class IKEProposalForm(NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
-        (_('Proposal'), ('name', 'description', 'tags')),
-        (_('Parameters'), (
+        FieldSet('name', 'description', 'tags', name=_('Proposal')),
+        FieldSet(
             'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
             'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime',
-        )),
+            name=_('Parameters')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -318,8 +317,8 @@ class IKEPolicyForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Policy'), ('name', 'description', 'tags')),
-        (_('Parameters'), ('version', 'mode', 'proposals', 'preshared_key')),
+        FieldSet('name', 'description', 'tags', name=_('Policy')),
+        FieldSet('version', 'mode', 'proposals', 'preshared_key', name=_('Parameters')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -332,10 +331,11 @@ class IKEPolicyForm(NetBoxModelForm):
 class IPSecProposalForm(NetBoxModelForm):
 class IPSecProposalForm(NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
-        (_('Proposal'), ('name', 'description', 'tags')),
-        (_('Parameters'), (
+        FieldSet('name', 'description', 'tags', name=_('Proposal')),
+        FieldSet(
             'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
             'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data',
-        )),
+            name=_('Parameters')
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -353,8 +353,8 @@ class IPSecPolicyForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (_('Policy'), ('name', 'description', 'tags')),
-        (_('Parameters'), ('proposals', 'pfs_group')),
+        FieldSet('name', 'description', 'tags', name=_('Policy')),
+        FieldSet('proposals', 'pfs_group', name=_('Parameters')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -376,8 +376,8 @@ class IPSecProfileForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Profile'), ('name', 'description', 'tags')),
-        (_('Parameters'), ('mode', 'ike_policy', 'ipsec_policy')),
+        FieldSet('name', 'description', 'tags', name=_('Profile')),
+        FieldSet('mode', 'ike_policy', 'ipsec_policy', name=_('Parameters')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -406,9 +406,9 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
-        (_('Route Targets'), ('import_targets', 'export_targets')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
+        FieldSet('name', 'slug', 'type', 'identifier', 'description', 'tags', name=_('L2VPN')),
+        FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -446,15 +446,15 @@ class L2VPNTerminationForm(NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        (None, (
+        FieldSet(
             'l2vpn',
             'l2vpn',
             TabbedGroups(
             TabbedGroups(
-                (_('VLAN'), 'vlan'),
-                (_('Device'), 'interface'),
-                (_('Virtual Machine'), 'vminterface'),
+                FieldSet('vlan', name=_('VLAN')),
+                FieldSet('interface', name=_('Device')),
+                FieldSet('vminterface', name=_('Virtual Machine')),
             ),
             ),
             'tags',
             'tags',
-        )),
+        ),
     )
     )
 
 
     class Meta:
     class Meta:

+ 6 - 5
netbox/wireless/forms/bulk_edit.py

@@ -7,6 +7,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import CommentField, DynamicModelChoiceField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField
+from utilities.forms.rendering import FieldSet
 from wireless.choices import *
 from wireless.choices import *
 from wireless.constants import SSID_MAX_LENGTH
 from wireless.constants import SSID_MAX_LENGTH
 from wireless.models import *
 from wireless.models import *
@@ -32,7 +33,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = WirelessLANGroup
     model = WirelessLANGroup
     fieldsets = (
     fieldsets = (
-        (None, ('parent', 'description')),
+        FieldSet('parent', 'description'),
     )
     )
     nullable_fields = ('parent', 'description')
     nullable_fields = ('parent', 'description')
 
 
@@ -86,8 +87,8 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = WirelessLAN
     model = WirelessLAN
     fieldsets = (
     fieldsets = (
-        (None, ('group', 'ssid', 'status', 'vlan', 'tenant', 'description')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
+        FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
     nullable_fields = (
     nullable_fields = (
         'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
         'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
@@ -133,8 +134,8 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = WirelessLink
     model = WirelessLink
     fieldsets = (
     fieldsets = (
-        (None, ('ssid', 'status', 'tenant', 'description')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk'))
+        FieldSet('ssid', 'status', 'tenant', 'description'),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication'))
     )
     )
     nullable_fields = (
     nullable_fields = (
         'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
         'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',

+ 9 - 8
netbox/wireless/forms/filtersets.py

@@ -6,6 +6,7 @@ from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.rendering import FieldSet
 from wireless.choices import *
 from wireless.choices import *
 from wireless.models import *
 from wireless.models import *
 
 
@@ -29,10 +30,10 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm):
 class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = WirelessLAN
     model = WirelessLAN
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('ssid', 'group_id', 'status')),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
     ssid = forms.CharField(
     ssid = forms.CharField(
         required=False,
         required=False,
@@ -69,10 +70,10 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = WirelessLink
     model = WirelessLink
     fieldsets = (
     fieldsets = (
-        (None, ('q', 'filter_id', 'tag')),
-        (_('Attributes'), ('ssid', 'status',)),
-        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
+        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('ssid', 'status', name=_('Attributes')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
     ssid = forms.CharField(
     ssid = forms.CharField(
         required=False,
         required=False,

+ 10 - 11
netbox/wireless/forms/model_forms.py

@@ -6,6 +6,7 @@ from ipam.models import VLAN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
+from utilities.forms.rendering import FieldSet
 from wireless.models import *
 from wireless.models import *
 
 
 __all__ = (
 __all__ = (
@@ -24,9 +25,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Wireless LAN Group'), (
-            'parent', 'name', 'slug', 'description', 'tags',
-        )),
+        FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -51,9 +50,9 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Wireless LAN'), ('ssid', 'group', 'vlan', 'status', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
+        FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -158,11 +157,11 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        (_('Side A'), ('site_a', 'location_a', 'device_a', 'interface_a')),
-        (_('Side B'), ('site_b', 'location_b', 'device_b', 'interface_b')),
-        (_('Link'), ('status', 'ssid', 'description', 'tags')),
-        (_('Tenancy'), ('tenant_group', 'tenant')),
-        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
+        FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')),
+        FieldSet('site_b', 'location_b', 'device_b', 'interface_b', name=_('Side B')),
+        FieldSet('status', 'ssid', 'description', 'tags', name=_('Link')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
 
 
     class Meta:
     class Meta: