From 32c08fc35822c9f7d9db470e0c5fad1285e3c9aa Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 18 Mar 2024 09:09:50 -0500 Subject: [PATCH 01/44] Closes: #13918 - Add facility field (#15456) * Fixes: #13918 - Add facilities field to Location model. * Stupidly forgot to `git add` * Fix errant reference to site. * Misc cleanup --------- Co-authored-by: Jeremy Stretch --- docs/models/dcim/location.md | 4 ++++ netbox/dcim/api/serializers_/sites.py | 4 ++-- netbox/dcim/filtersets.py | 3 ++- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 4 ++-- .../dcim/migrations/0186_location_facility.py | 18 ++++++++++++++++++ netbox/dcim/models/sites.py | 8 +++++++- netbox/dcim/search.py | 3 ++- netbox/dcim/tables/sites.py | 8 +++++--- netbox/dcim/tests/test_filtersets.py | 10 +++++++--- netbox/dcim/tests/test_views.py | 1 + netbox/templates/dcim/location.html | 4 ++++ 12 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 netbox/dcim/migrations/0186_location_facility.py diff --git a/docs/models/dcim/location.md b/docs/models/dcim/location.md index 96ab13039..cf957ca5b 100644 --- a/docs/models/dcim/location.md +++ b/docs/models/dcim/location.md @@ -26,3 +26,7 @@ The location's operational status. !!! tip Additional statuses may be defined by setting `Location.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + +### Facility + +Data center or facility designation for identifying the location. diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index 6fb3811ba..8063278a7 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -92,7 +92,7 @@ class LocationSerializer(NestedGroupModelSerializer): class Meta: model = Location fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description', + 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index aa8a68296..2ff9f49ae 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -270,13 +270,14 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM class Meta: model = Location - fields = ('id', 'name', 'slug', 'status', 'description') + fields = ('id', 'name', 'slug', 'status', 'facility', 'description') def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( Q(name__icontains=value) | + Q(facility__icontains=value) | Q(description__icontains=value) ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 47974096f..d49973082 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -157,7 +157,7 @@ class LocationImportForm(NetBoxModelImportForm): class Meta: model = Location - fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags') + fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags') def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 6c33ea8d6..92740ec45 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -179,14 +179,14 @@ class LocationForm(TenancyForm, NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')), + (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags')), (_('Tenancy'), ('tenant_group', 'tenant')), ) class Meta: model = Location fields = ( - 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags', + 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags', ) diff --git a/netbox/dcim/migrations/0186_location_facility.py b/netbox/dcim/migrations/0186_location_facility.py new file mode 100644 index 000000000..759ee813b --- /dev/null +++ b/netbox/dcim/migrations/0186_location_facility.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.4 on 2024-03-17 02:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0185_gfk_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='location', + name='facility', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index d2797bf95..c1da807ad 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -275,6 +275,12 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel): blank=True, null=True ) + facility = models.CharField( + verbose_name=_('facility'), + max_length=50, + blank=True, + help_text=_('Local facility ID or description') + ) # Generic relations vlan_groups = GenericRelation( @@ -284,7 +290,7 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel): related_query_name='location' ) - clone_fields = ('site', 'parent', 'status', 'tenant', 'description') + clone_fields = ('site', 'parent', 'status', 'tenant', 'facility', 'description') prerequisite_models = ( 'dcim.Site', ) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index 18cf75a9a..b349bcac0 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -132,10 +132,11 @@ class LocationIndex(SearchIndex): model = models.Location fields = ( ('name', 100), + ('facility', 100), ('slug', 110), ('description', 500), ) - display_attrs = ('site', 'status', 'tenant', 'description') + display_attrs = ('site', 'status', 'tenant', 'facility', 'description') @register_search diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index a0a71ab30..e179ec43a 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -152,7 +152,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Location fields = ( - 'pk', 'id', 'name', 'site', 'status', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', - 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated', + 'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count', + 'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description' ) - default_columns = ('pk', 'name', 'site', 'status', 'tenant', 'rack_count', 'device_count', 'description') diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 1e46d66ac..fffa82a10 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -359,9 +359,9 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): location.save() locations = ( - Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'), - Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'), - Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'), + Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, facility='Facility 1', description='foobar1'), + Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, facility='Facility 2', description='foobar2'), + Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, facility='Facility 3', description='foobar3'), ) for location in locations: location.save() @@ -390,6 +390,10 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'status': [LocationStatusChoices.STATUS_PLANNED, LocationStatusChoices.STATUS_STAGING]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_facility(self): + params = {'facility': ['Facility 1', 'Facility 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index e9e5a557b..e3437cefc 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -213,6 +213,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'slug': 'location-x', 'site': site.pk, 'status': LocationStatusChoices.STATUS_PLANNED, + 'facility': 'Facility X', 'tenant': tenant.pk, 'description': 'A new location', 'tags': [t.pk for t in tags], diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index db387b164..9f2b766ea 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -54,6 +54,10 @@ {{ object.tenant|linkify|placeholder }} + + {% trans "Facility" %} + {{ object.facility|placeholder }} + {% include 'inc/panels/tags.html' %} From 2c12f03ca1b3d8531766e7c0198d02f09ffcca73 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 18 Mar 2024 11:26:53 -0700 Subject: [PATCH 02/44] 15193 use psycopg compiled --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 07add11a2..72b086912 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ mkdocstrings[python-legacy]==0.24.1 netaddr==1.2.1 nh3==0.2.15 Pillow==10.2.0 -psycopg[binary,pool]==3.1.18 +psycopg[c,pool]==3.1.18 PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 From 251688dca83e8288111fdde12beafadcf130c2b3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Mar 2024 15:44:35 -0400 Subject: [PATCH 03/44] Introduce InlineFields for rendering fields side-by-side --- netbox/dcim/forms/model_forms.py | 17 ++++ netbox/dcim/views.py | 1 - netbox/templates/dcim/rack_edit.html | 90 ------------------- netbox/templates/htmx/form.html | 17 +--- netbox/utilities/forms/rendering.py | 10 +++ .../form_helpers/render_fieldset.html | 26 ++++++ netbox/utilities/templatetags/form_helpers.py | 24 +++++ 7 files changed, 79 insertions(+), 106 deletions(-) delete mode 100644 netbox/templates/dcim/rack_edit.html create mode 100644 netbox/utilities/forms/rendering.py create mode 100644 netbox/utilities/templates/form_helpers/render_fieldset.html diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 92740ec45..44c3bb40a 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,6 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) +from utilities.forms.rendering import InlineFields from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -227,6 +228,22 @@ class RackForm(TenancyForm, NetBoxModelForm): ) comments = CommentField() + 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', + InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), + InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), + 'mounting_depth', + 'desc_units', + )), + ) + class Meta: model = Rack fields = [ diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 93e5f04dc..b447ae579 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -727,7 +727,6 @@ class RackNonRackedView(generic.ObjectChildrenView): class RackEditView(generic.ObjectEditView): queryset = Rack.objects.all() form = forms.RackForm - template_name = 'dcim/rack_edit.html' @register_model_view(Rack, 'delete') diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html deleted file mode 100644 index 21bc8303d..000000000 --- a/netbox/templates/dcim/rack_edit.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Rack" %}
-
- {% render_field form.site %} - {% render_field form.location %} - {% render_field form.name %} - {% render_field form.status %} - {% render_field form.role %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Inventory Control" %}
-
- {% render_field form.facility_id %} - {% render_field form.serial %} - {% render_field form.asset_tag %} -
- -
-
-
{% trans "Tenancy" %}
-
- {% render_field form.tenant_group %} - {% render_field form.tenant %} -
- -
-
-
{% trans "Dimensions" %}
-
- {% render_field form.type %} - {% render_field form.width %} - {% render_field form.starting_unit %} - {% render_field form.u_height %} -
- -
- {{ form.outer_width }} -
{% trans "Width" %}
-
-
- {{ form.outer_depth }} -
{% trans "Depth" %}
-
-
- {{ form.outer_unit }} -
{% trans "Unit" %}
-
-
-
- -
- {{ form.weight }} -
{% trans "Weight" %}
-
-
- {{ form.max_weight }} -
{% trans "Maximum Weight" %}
-
-
- {{ form.weight_unit }} -
{% trans "Unit" %}
-
-
- {% render_field form.mounting_depth %} - {% render_field form.desc_units %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} - -
- {% render_field form.comments %} -
-{% endblock %} diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 3aafc2a21..0bfcb00ca 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -9,21 +9,8 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, fields in form.fieldsets %} -
- {% if group %} -
-
{{ group }}
-
- {% endif %} - {% for name in fields %} - {% with field=form|getfield:name %} - {% if field and not field.field.widget.is_hidden %} - {% render_field field %} - {% endif %} - {% endwith %} - {% endfor %} -
+ {% for group, items in form.fieldsets %} + {% render_fieldset form items heading=group %} {% endfor %} {% if form.custom_fields %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py new file mode 100644 index 000000000..498b1a2ce --- /dev/null +++ b/netbox/utilities/forms/rendering.py @@ -0,0 +1,10 @@ +__all__ = ( + 'InlineFields', +) + + +class InlineFields: + + def __init__(self, *field_names, label=None): + self.field_names = field_names + self.label = label diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html new file mode 100644 index 000000000..718a8f6a0 --- /dev/null +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -0,0 +1,26 @@ +{% load i18n %} +{% load form_helpers %} +
+ {% if heading %} +
+
{{ heading }}
+
+ {% endif %} + {% for layout, title, items in rows %} + {% if layout == 'field' %} + {# Single form field #} + {% render_field items.0 %} + {% elif layout == 'inline' %} + {# Multiple form fields on the same line #} +
+ + {% for field in items %} +
+ {{ field }} +
{% trans field.label %}
+
+ {% endfor %} +
+ {% endif %} + {% endfor %} +
diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index f4fd8b819..3f60627b4 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,5 +1,7 @@ from django import template +from utilities.forms.rendering import InlineFields + __all__ = ( 'getfield', 'render_custom_fields', @@ -45,6 +47,28 @@ def widget_type(field): # Inclusion tags # +@register.inclusion_tag('form_helpers/render_fieldset.html') +def render_fieldset(form, fieldset, heading=None): + """ + Render a group set of fields. + """ + rows = [] + for item in fieldset: + if type(item) is InlineFields: + rows.append( + ('inline', item.label, [form[name] for name in item.field_names]) + ) + else: + rows.append( + ('field', None, [form[item]]) + ) + + return { + 'heading': heading, + 'rows': rows, + } + + @register.inclusion_tag('form_helpers/render_field.html') def render_field(field, bulk_nullable=False, label=None): """ From 6bc5eed79da66ee8940ce7ed1405c57eab732c0e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Mar 2024 17:02:26 -0400 Subject: [PATCH 04/44] Enable tabbed group fields in fieldsets --- netbox/dcim/forms/model_forms.py | 24 ++-- netbox/dcim/views.py | 2 - netbox/templates/dcim/inventoryitem_edit.html | 107 ------------------ netbox/utilities/forms/rendering.py | 37 +++++- .../form_helpers/render_fieldset.html | 26 +++++ netbox/utilities/templatetags/form_helpers.py | 17 ++- 6 files changed, 92 insertions(+), 121 deletions(-) delete mode 100644 netbox/templates/dcim/inventoryitem_edit.html diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 44c3bb40a..06f28b4e6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields +from utilities.forms.rendering import InlineFields, TabbedFieldGroups from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm): 'width', 'starting_unit', 'u_height', - InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), - InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), + InlineFields(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'), + InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'), 'mounting_depth', 'desc_units', )), @@ -1414,6 +1414,17 @@ class InventoryItemForm(DeviceComponentForm): fieldsets = ( (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), + (_('Component Assignment'), ( + TabbedFieldGroups( + (_('Interface'), 'interface'), + (_('Console Port'), 'consoleport'), + (_('Console Server Port'), 'consoleserverport'), + (_('Front Port'), 'frontport'), + (_('Rear Port'), 'rearport'), + (_('Power Port'), 'powerport'), + (_('Power Outlet'), 'poweroutlet'), + ), + )) ) class Meta: @@ -1429,22 +1440,17 @@ class InventoryItemForm(DeviceComponentForm): component_type = initial.get('component_type') component_id = initial.get('component_id') - # Used for picking the default active tab for component selection - self.no_component = True - if instance: - # When editing set the initial value for component selectin + # When editing set the initial value for component selection for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS): if type(instance.component) is component_model.model_class(): initial[component_model.model] = instance.component - self.no_component = False break elif component_type and component_id: # When adding the InventoryItem from a component page if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first(): if component := content_type.model_class().objects.filter(pk=component_id).first(): initial[content_type.model] = component - self.no_component = False kwargs['initial'] = initial diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b447ae579..49bbe9be1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2924,14 +2924,12 @@ class InventoryItemView(generic.ObjectView): class InventoryItemEditView(generic.ObjectEditView): queryset = InventoryItem.objects.all() form = forms.InventoryItemForm - template_name = 'dcim/inventoryitem_edit.html' class InventoryItemCreateView(generic.ComponentCreateView): queryset = InventoryItem.objects.all() form = forms.InventoryItemCreateForm model_form = forms.InventoryItemForm - template_name = 'dcim/inventoryitem_edit.html' @register_model_view(InventoryItem, 'delete') diff --git a/netbox/templates/dcim/inventoryitem_edit.html b/netbox/templates/dcim/inventoryitem_edit.html deleted file mode 100644 index 1dc46ddce..000000000 --- a/netbox/templates/dcim/inventoryitem_edit.html +++ /dev/null @@ -1,107 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Inventory Item" %}
-
- {% render_field form.device %} - {% render_field form.parent %} - {% render_field form.name %} - {% render_field form.label %} - {% render_field form.role %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Hardware" %}
-
- {% render_field form.manufacturer %} - {% render_field form.part_id %} - {% render_field form.serial %} - {% render_field form.asset_tag %} -
- -
-
-
{% trans "Component Assignment" %}
-
-
- -
-
-
- {% render_field form.consoleport %} -
-
- {% render_field form.consoleserverport %} -
-
- {% render_field form.frontport %} -
-
- {% render_field form.interface %} -
-
- {% render_field form.poweroutlet %} -
-
- {% render_field form.powerport %} -
-
- {% render_field form.rearport %} -
-
-
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index 498b1a2ce..ad87930a9 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -1,10 +1,43 @@ +import random +import string +from functools import cached_property + __all__ = ( + 'FieldGroup', 'InlineFields', + 'TabbedFieldGroups', ) -class InlineFields: +class FieldGroup: - def __init__(self, *field_names, label=None): + def __init__(self, label, *field_names): self.field_names = field_names self.label = label + + +class InlineFields(FieldGroup): + pass + + +class TabbedFieldGroups: + + def __init__(self, *groups): + self.groups = [ + FieldGroup(*group) for group in groups + ] + + # Initialize a random ID for the group (for tab selection) + self.id = ''.join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(8) + ) + + @cached_property + def tabs(self): + return [ + { + 'id': f'{self.id}_{i}', + 'title': group.label, + 'fields': group.field_names, + } for i, group in enumerate(self.groups, start=1) + ] diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index 718a8f6a0..ee1f50293 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -7,9 +7,11 @@ {% endif %} {% for layout, title, items in rows %} + {% if layout == 'field' %} {# Single form field #} {% render_field items.0 %} + {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
@@ -21,6 +23,30 @@
{% endfor %} + + {% elif layout == 'tabs' %} + {# Tabbed groups of fields #} +
+ +
+
+ {% for tab in items %} +
+ {% for field in tab.fields %} + {% render_field field %} + {% endfor %} +
+ {% endfor %} +
+ {% endif %} {% endfor %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 3f60627b4..47bbaafe8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields +from utilities.forms.rendering import InlineFields, TabbedFieldGroups __all__ = ( 'getfield', @@ -58,6 +58,21 @@ def render_fieldset(form, fieldset, heading=None): rows.append( ('inline', item.label, [form[name] for name in item.field_names]) ) + elif type(item) is TabbedFieldGroups: + tabs = [ + { + 'id': tab['id'], + 'title': tab['title'], + 'active': bool(form.initial.get(tab['fields'][0], False)), + 'fields': [form[name] for name in tab['fields']] + } for tab in item.tabs + ] + # If none of the tabs has been marked as active, activate the first one + if not any(tab['active'] for tab in tabs): + tabs[0]['active'] = True + rows.append( + ('tabs', None, tabs) + ) else: rows.append( ('field', None, [form[item]]) From 1f647540ab59a13509aeb6fe16fff843a8bb1ddf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 09:43:01 -0400 Subject: [PATCH 05/44] Ignore fields which are not included on the form (dynamic rendering) --- netbox/utilities/templatetags/form_helpers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 47bbaafe8..c55a6b98b 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -54,17 +54,24 @@ def render_fieldset(form, fieldset, heading=None): """ rows = [] for item in fieldset: + + # Multiple fields side-by-side if type(item) is InlineFields: + fields = [ + form[name] for name in item.field_names if name in form.fields + ] rows.append( - ('inline', item.label, [form[name] for name in item.field_names]) + ('inline', item.label, fields) ) + + # Tabbed groups of fields elif type(item) is TabbedFieldGroups: tabs = [ { 'id': tab['id'], 'title': tab['title'], 'active': bool(form.initial.get(tab['fields'][0], False)), - 'fields': [form[name] for name in tab['fields']] + 'fields': [form[name] for name in tab['fields'] if name in form.fields] } for tab in item.tabs ] # If none of the tabs has been marked as active, activate the first one @@ -73,7 +80,9 @@ def render_fieldset(form, fieldset, heading=None): rows.append( ('tabs', None, tabs) ) - else: + + # A single form field + elif item in form.fields: rows.append( ('field', None, [form[item]]) ) From f28ea2d6f6470d66d6cc3e68e5b70e1ef374a541 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 10:15:34 -0400 Subject: [PATCH 06/44] Introduce ObjectAttribute for displaying read-only instance attributes on forms --- netbox/extras/forms/model_forms.py | 4 +++ netbox/extras/views.py | 1 - netbox/ipam/forms/model_forms.py | 5 +++ netbox/ipam/views.py | 1 - .../extras/imageattachment_edit.html | 19 ---------- .../ipam/fhrpgroupassignment_edit.html | 19 ---------- .../tenancy/contactassignment_edit.html | 35 ------------------- netbox/tenancy/forms/model_forms.py | 5 +++ netbox/tenancy/views.py | 1 - netbox/utilities/forms/rendering.py | 8 ++++- .../form_helpers/render_fieldset.html | 11 ++++++ netbox/utilities/templatetags/form_helpers.py | 9 ++++- 12 files changed, 40 insertions(+), 78 deletions(-) delete mode 100644 netbox/templates/extras/imageattachment_edit.html delete mode 100644 netbox/templates/ipam/fhrpgroupassignment_edit.html delete mode 100644 netbox/templates/tenancy/contactassignment_edit.html diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 09d2d9535..4e62b3ab7 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -17,6 +17,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) +from utilities.forms.rendering import ObjectAttribute from utilities.forms.widgets import ChoicesWidget, HTMXSelect from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -526,6 +527,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm): + fieldsets = ( + (None, (ObjectAttribute('parent'), 'name', 'image')), + ) class Meta: model = ImageAttachment diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 1fa2a30aa..cb3fdd39c 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -759,7 +759,6 @@ class ImageAttachmentListView(generic.ObjectListView): class ImageAttachmentEditView(generic.ObjectEditView): queryset = ImageAttachment.objects.all() form = forms.ImageAttachmentForm - template_name = 'extras/imageattachment_edit.html' def alter_object(self, instance, request, args, kwargs): if not instance.pk: diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 47087139a..07f782f7f 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,6 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) +from utilities.forms.rendering import ObjectAttribute from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -502,6 +503,10 @@ class FHRPGroupAssignmentForm(forms.ModelForm): queryset=FHRPGroup.objects.all() ) + fieldsets = ( + (None, (ObjectAttribute('interface'), 'group', 'priority')), + ) + class Meta: model = FHRPGroupAssignment fields = ('group', 'priority') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 9c4a9a102..79716f082 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1059,7 +1059,6 @@ class FHRPGroupBulkDeleteView(generic.BulkDeleteView): class FHRPGroupAssignmentEditView(generic.ObjectEditView): queryset = FHRPGroupAssignment.objects.all() form = forms.FHRPGroupAssignmentForm - template_name = 'ipam/fhrpgroupassignment_edit.html' def alter_object(self, instance, request, args, kwargs): if not instance.pk: diff --git a/netbox/templates/extras/imageattachment_edit.html b/netbox/templates/extras/imageattachment_edit.html deleted file mode 100644 index 75b2ce48b..000000000 --- a/netbox/templates/extras/imageattachment_edit.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block form %} -
-
- -
-
- {{ object.parent|linkify }} -
-
-
- {% render_form form %} -
-{% endblock form %} diff --git a/netbox/templates/ipam/fhrpgroupassignment_edit.html b/netbox/templates/ipam/fhrpgroupassignment_edit.html deleted file mode 100644 index bbc1505f2..000000000 --- a/netbox/templates/ipam/fhrpgroupassignment_edit.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "FHRP Group Assignment" %}
-
-
- -
- -
-
- {% render_field form.group %} - {% render_field form.priority %} -
-{% endblock %} diff --git a/netbox/templates/tenancy/contactassignment_edit.html b/netbox/templates/tenancy/contactassignment_edit.html deleted file mode 100644 index 342debcbb..000000000 --- a/netbox/templates/tenancy/contactassignment_edit.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} -
-
-
{% trans "Contact Assignment" %}
-
-
- -
- -
-
- {% render_field form.group %} - {% render_field form.contact %} - {% render_field form.role %} - {% render_field form.priority %} - {% render_field form.tags %} -
- -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
-{% endblock %} diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 140d9cf9a..7dcb4e433 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.rendering import ObjectAttribute __all__ = ( 'ContactAssignmentForm', @@ -140,6 +141,10 @@ class ContactAssignmentForm(NetBoxModelForm): queryset=ContactRole.objects.all() ) + fieldsets = ( + (None, (ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags')), + ) + class Meta: model = ContactAssignment fields = ( diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 4c4d263df..d30793a16 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -369,7 +369,6 @@ class ContactAssignmentListView(generic.ObjectListView): class ContactAssignmentEditView(generic.ObjectEditView): queryset = ContactAssignment.objects.all() form = forms.ContactAssignmentForm - template_name = 'tenancy/contactassignment_edit.html' def alter_object(self, instance, request, args, kwargs): if not instance.pk: diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index ad87930a9..d60f3f061 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -3,8 +3,8 @@ import string from functools import cached_property __all__ = ( - 'FieldGroup', 'InlineFields', + 'ObjectAttribute', 'TabbedFieldGroups', ) @@ -41,3 +41,9 @@ class TabbedFieldGroups: 'fields': group.field_names, } for i, group in enumerate(self.groups, start=1) ] + + +class ObjectAttribute: + + def __init__(self, name): + self.name = name diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index ee1f50293..d4c7981f7 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -12,6 +12,17 @@ {# Single form field #} {% render_field items.0 %} + {% elif layout == 'attribute' %} + {# A static attribute of the form's instance #} +
+ +
+
+ {{ items.0|linkify }} +
+
+
+ {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index c55a6b98b..e336ac21b 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups __all__ = ( 'getfield', @@ -81,6 +81,13 @@ def render_fieldset(form, fieldset, heading=None): ('tabs', None, tabs) ) + elif type(item) is ObjectAttribute: + value = getattr(form.instance, item.name) + label = value._meta.verbose_name if hasattr(value, '_meta') else item.name + rows.append( + ('attribute', label.title(), [value]) + ) + # A single form field elif item in form.fields: rows.append( From 9834c6501b99c342e2e002facd4f2c026588ea03 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 10:59:00 -0400 Subject: [PATCH 07/44] Replace custom form templates with TabbedFieldGroups --- netbox/circuits/forms/model_forms.py | 16 ++++ netbox/circuits/views.py | 1 - netbox/ipam/forms/model_forms.py | 46 ++++++++- netbox/ipam/views.py | 3 - .../circuits/circuittermination_edit.html | 58 ------------ netbox/templates/ipam/ipaddress_edit.html | 93 ------------------- netbox/templates/ipam/service_create.html | 79 ---------------- netbox/templates/ipam/service_edit.html | 66 ------------- .../templates/vpn/l2vpntermination_edit.html | 56 ----------- .../form_helpers/render_fieldset.html | 24 ++--- netbox/vpn/forms/model_forms.py | 13 +++ netbox/vpn/views.py | 1 - 12 files changed, 87 insertions(+), 369 deletions(-) delete mode 100644 netbox/templates/circuits/circuittermination_edit.html delete mode 100644 netbox/templates/ipam/ipaddress_edit.html delete mode 100644 netbox/templates/ipam/service_create.html delete mode 100644 netbox/templates/ipam/service_edit.html delete mode 100644 netbox/templates/vpn/l2vpntermination_edit.html diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 0809cb2f4..9e29f6477 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,6 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.rendering import TabbedFieldGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -146,6 +147,21 @@ class CircuitTerminationForm(NetBoxModelForm): selector=True ) + fieldsets = ( + (_('Circuit Termination'), ( + 'circuit', + 'term_side', + 'description', + 'tags', + TabbedFieldGroups( + (_('Site'), 'site'), + (_('Provider Network'), 'provider_network'), + ), + 'mark_connected', + )), + (_('Termination Details'), ('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info')), + ) + class Meta: model = CircuitTermination fields = [ diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 64dd82682..0c01d6eb9 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -412,7 +412,6 @@ class CircuitContactsView(ObjectContactsView): class CircuitTerminationEditView(generic.ObjectEditView): queryset = CircuitTermination.objects.all() form = forms.CircuitTerminationForm - template_name = 'circuits/circuittermination_edit.html' @register_model_view(CircuitTermination, 'delete') diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 07f782f7f..85f9591a8 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -308,6 +308,20 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), + (_('Tenancy'), ('tenant_group', 'tenant')), + (_('Assignment'), ( + TabbedFieldGroups( + (_('Device'), 'interface'), + (_('Virtual Machine'), 'vminterface'), + (_('FHRP Group'), 'fhrpgroup'), + ), + 'primary_for_parent', + )), + (_('NAT IP (Inside)'), ('nat_inside',)), + ) + class Meta: model = IPAddress fields = [ @@ -709,6 +723,20 @@ class ServiceForm(NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + (_('Service'), ( + TabbedFieldGroups( + (_('Device'), 'device'), + (_('Virtual Machine'), 'virtual_machine'), + ), + 'name', + InlineFields(_('Port(s)'), 'protocol', 'ports'), + 'ipaddresses', + 'description', + 'tags', + )), + ) + class Meta: model = Service fields = [ @@ -723,6 +751,22 @@ class ServiceCreateForm(ServiceForm): required=False ) + fieldsets = ( + (_('Service'), ( + TabbedFieldGroups( + (_('Device'), 'device'), + (_('Virtual Machine'), 'virtual_machine'), + ), + TabbedFieldGroups( + (_('From Template'), 'service_template'), + (_('Custom'), 'name', 'protocol', 'ports'), + ), + 'ipaddresses', + 'description', + 'tags', + )), + ) + class Meta(ServiceForm.Meta): fields = [ 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description', diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 79716f082..6870d1e9e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -781,7 +781,6 @@ class IPAddressView(generic.ObjectView): class IPAddressEditView(generic.ObjectEditView): queryset = IPAddress.objects.all() form = forms.IPAddressForm - template_name = 'ipam/ipaddress_edit.html' def alter_object(self, obj, request, url_args, url_kwargs): @@ -1235,14 +1234,12 @@ class ServiceView(generic.ObjectView): class ServiceCreateView(generic.ObjectEditView): queryset = Service.objects.all() form = forms.ServiceCreateForm - template_name = 'ipam/service_create.html' @register_model_view(Service, 'edit') class ServiceEditView(generic.ObjectEditView): queryset = Service.objects.all() form = forms.ServiceForm - template_name = 'ipam/service_edit.html' @register_model_view(Service, 'delete') diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html deleted file mode 100644 index 18198cb72..000000000 --- a/netbox/templates/circuits/circuittermination_edit.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Circuit Termination" %}
-
- {% render_field form.circuit %} - {% render_field form.term_side %} - {% render_field form.tags %} - {% render_field form.mark_connected %} - {% with providernetwork_tab_active=form.initial.provider_network %} -
-
- -
-
-
-
- {% render_field form.site %} -
-
- {% render_field form.provider_network %} -
-
- {% endwith %} -
- -
-
-
{% trans "Termination Details" %}
-
- {% render_field form.port_speed %} - {% render_field form.upstream_speed %} - {% render_field form.xconnect_id %} - {% render_field form.pp_info %} - {% render_field form.description %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html deleted file mode 100644 index d9157f5ef..000000000 --- a/netbox/templates/ipam/ipaddress_edit.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load helpers %} -{% load i18n %} - -{% block tabs %} - {% include 'ipam/inc/ipaddress_edit_header.html' with active_tab='add' %} -{% endblock tabs %} - -{% block form %} -
-
-
{% trans "IP Address" %}
-
- {% render_field form.address %} - {% render_field form.status %} - {% render_field form.role %} - {% render_field form.vrf %} - {% render_field form.dns_name %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Tenancy" %}
-
- {% render_field form.tenant_group %} - {% render_field form.tenant %} -
- -
-
-
{% trans "Interface Assignment" %}
-
-
-
- -
-
-
-
- {% render_field form.interface %} -
-
- {% render_field form.vminterface %} -
-
- {% render_field form.fhrpgroup %} -
- {% render_field form.primary_for_parent %} -
-
- -
-
-
{% trans "NAT IP (Inside" %})
-
-
- {% render_field form.nat_inside %} -
-
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/service_create.html b/netbox/templates/ipam/service_create.html deleted file mode 100644 index d145999c0..000000000 --- a/netbox/templates/ipam/service_create.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Service" %}
-
- - {# Device/VM selection #} -
-
- -
-
-
-
- {% render_field form.device %} -
-
- {% render_field form.virtual_machine %} -
-
- - {# Template or custom #} -
-
- -
-
-
-
- {% render_field form.service_template %} -
-
- {% render_field form.name %} - {% render_field form.protocol %} - {% render_field form.ports %} -
-
- {% render_field form.ipaddresses %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} - {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/service_edit.html b/netbox/templates/ipam/service_edit.html deleted file mode 100644 index 33eda76e1..000000000 --- a/netbox/templates/ipam/service_edit.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Service" %}
-
- -
-
- -
-
-
-
- {% render_field form.device %} -
-
- {% render_field form.virtual_machine %} -
-
- {% render_field form.name %} -
- -
- {{ form.protocol }} -
-
- {{ form.ports }} -
-
-
-
-
- {{ form.ports.help_text }} -
-
- {% render_field form.ipaddresses %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} - {% endif %} -{% endblock %} diff --git a/netbox/templates/vpn/l2vpntermination_edit.html b/netbox/templates/vpn/l2vpntermination_edit.html deleted file mode 100644 index 14b30c78d..000000000 --- a/netbox/templates/vpn/l2vpntermination_edit.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "L2VPN Termination" %}
-
- {% render_field form.l2vpn %} -
-
- -
-
-
-
-
- {% render_field form.vlan %} -
-
- {% render_field form.interface %} -
-
- {% render_field form.vminterface %} -
- {% render_field form.tags %} -
-
-
- {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
-{% endif %} -{% endblock %} diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index d4c7981f7..d978997af 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -26,7 +26,7 @@ {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
- + {% for field in items %}
{{ field }} @@ -37,16 +37,18 @@ {% elif layout == 'tabs' %} {# Tabbed groups of fields #} -
- +
+
+ +
{% for tab in items %} diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 9e5e17a09..efb8a7eda 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,6 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.rendering import TabbedFieldGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -444,6 +445,18 @@ class L2VPNTerminationForm(NetBoxModelForm): label=_('Interface') ) + fieldsets = ( + (None, ( + 'l2vpn', + TabbedFieldGroups( + (_('VLAN'), 'vlan'), + (_('Device'), 'interface'), + (_('Virtual Machine'), 'vminterface'), + ), + 'tags', + )), + ) + class Meta: model = L2VPNTermination fields = ('l2vpn', 'tags') diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 9bf424af9..af1f653c8 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -479,7 +479,6 @@ class L2VPNTerminationView(generic.ObjectView): class L2VPNTerminationEditView(generic.ObjectEditView): queryset = L2VPNTermination.objects.all() form = forms.L2VPNTerminationForm - template_name = 'vpn/l2vpntermination_edit.html' @register_model_view(L2VPNTermination, 'delete') From 2730bd57128daa449025eeae4ea469fc0a03fcc1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Mar 2024 12:59:42 -0400 Subject: [PATCH 08/44] Refactor form rendering components & add docstrings --- netbox/circuits/forms/model_forms.py | 4 +- netbox/dcim/forms/model_forms.py | 8 ++-- netbox/ipam/forms/model_forms.py | 12 +++--- netbox/templates/htmx/form.html | 4 +- netbox/utilities/forms/rendering.py | 41 ++++++++++++------- netbox/utilities/templatetags/form_helpers.py | 17 +++++--- netbox/vpn/forms/model_forms.py | 4 +- 7 files changed, 54 insertions(+), 36 deletions(-) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 9e29f6477..d73da3a02 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -153,7 +153,7 @@ class CircuitTerminationForm(NetBoxModelForm): 'term_side', 'description', 'tags', - TabbedFieldGroups( + TabbedGroups( (_('Site'), 'site'), (_('Provider Network'), 'provider_network'), ), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 06f28b4e6..e0c25dbba 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, TabbedGroups from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm): 'width', 'starting_unit', 'u_height', - InlineFields(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'), - InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'), + InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), + InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), 'mounting_depth', 'desc_units', )), @@ -1415,7 +1415,7 @@ class InventoryItemForm(DeviceComponentForm): (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), (_('Component Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Interface'), 'interface'), (_('Console Port'), 'consoleport'), (_('Console Server Port'), 'consoleserverport'), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 85f9591a8..0aba37fb9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -312,7 +312,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), (_('Tenancy'), ('tenant_group', 'tenant')), (_('Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'), (_('FHRP Group'), 'fhrpgroup'), @@ -725,12 +725,12 @@ class ServiceForm(NetBoxModelForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), 'name', - InlineFields(_('Port(s)'), 'protocol', 'ports'), + InlineFields('protocol', 'ports', label=_('Port(s)')), 'ipaddresses', 'description', 'tags', @@ -753,11 +753,11 @@ class ServiceCreateForm(ServiceForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), - TabbedFieldGroups( + TabbedGroups( (_('From Template'), 'service_template'), (_('Custom'), 'name', 'protocol', 'ports'), ), diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 0bfcb00ca..f9eecc2b9 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -9,8 +9,8 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, items in form.fieldsets %} - {% render_fieldset form items heading=group %} + {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} {% endfor %} {% if form.custom_fields %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index d60f3f061..ea73c38ff 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -3,28 +3,39 @@ import string from functools import cached_property __all__ = ( + 'FieldSet', 'InlineFields', 'ObjectAttribute', - 'TabbedFieldGroups', + 'TabbedGroups', ) -class FieldGroup: +class FieldSet: + """ + A generic grouping of fields, with an optional name. Each field will be rendered + on its own row under the heading (name). + """ + def __init__(self, *fields, name=None): + self.fields = fields + self.name = name - def __init__(self, label, *field_names): - self.field_names = field_names + +class InlineFields: + """ + A set of fields rendered inline (side-by-side) with a shared label; typically nested within a FieldSet. + """ + def __init__(self, *fields, label=None): + self.fields = fields self.label = label -class InlineFields(FieldGroup): - pass - - -class TabbedFieldGroups: - +class TabbedGroups: + """ + Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate. + """ def __init__(self, *groups): self.groups = [ - FieldGroup(*group) for group in groups + FieldSet(*group, name=name) for name, *group in groups ] # Initialize a random ID for the group (for tab selection) @@ -37,13 +48,15 @@ class TabbedFieldGroups: return [ { 'id': f'{self.id}_{i}', - 'title': group.label, - 'fields': group.field_names, + 'title': group.name, + 'fields': group.fields, } for i, group in enumerate(self.groups, start=1) ] class ObjectAttribute: - + """ + Renders the value for a specific attribute on the form's instance. + """ def __init__(self, name): self.name = name diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index e336ac21b..48a1a5aa8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups __all__ = ( 'getfield', @@ -48,24 +48,29 @@ def widget_type(field): # @register.inclusion_tag('form_helpers/render_fieldset.html') -def render_fieldset(form, fieldset, heading=None): +def render_fieldset(form, fieldset): """ Render a group set of fields. """ + # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3')) + if type(fieldset) is not FieldSet: + name, fields = fieldset + fieldset = FieldSet(*fields, name=name) + rows = [] - for item in fieldset: + for item in fieldset.fields: # Multiple fields side-by-side if type(item) is InlineFields: fields = [ - form[name] for name in item.field_names if name in form.fields + form[name] for name in item.fields if name in form.fields ] rows.append( ('inline', item.label, fields) ) # Tabbed groups of fields - elif type(item) is TabbedFieldGroups: + elif type(item) is TabbedGroups: tabs = [ { 'id': tab['id'], @@ -95,7 +100,7 @@ def render_fieldset(form, fieldset, heading=None): ) return { - 'heading': heading, + 'heading': fieldset.name, 'rows': rows, } diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index efb8a7eda..9674ee2f9 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -448,7 +448,7 @@ class L2VPNTerminationForm(NetBoxModelForm): fieldsets = ( (None, ( 'l2vpn', - TabbedFieldGroups( + TabbedGroups( (_('VLAN'), 'vlan'), (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'), From c928756396c4f184475a5c0e924637daf5515d0e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Mar 2024 15:08:28 -0400 Subject: [PATCH 09/44] Use FieldSet instances for all forms --- netbox/circuits/forms/bulk_edit.py | 15 +- netbox/circuits/forms/filtersets.py | 33 +-- netbox/circuits/forms/model_forms.py | 33 +-- netbox/core/forms/bulk_edit.py | 3 +- netbox/core/forms/filtersets.py | 23 +- netbox/core/forms/model_forms.py | 37 ++- netbox/dcim/forms/bulk_edit.py | 113 ++++---- netbox/dcim/forms/filtersets.py | 258 +++++++++--------- netbox/dcim/forms/model_forms.py | 207 +++++++------- netbox/dcim/forms/object_create.py | 7 +- netbox/extras/forms/filtersets.py | 69 ++--- netbox/extras/forms/model_forms.py | 71 ++--- netbox/ipam/forms/bulk_edit.py | 43 +-- netbox/ipam/forms/filtersets.py | 93 ++++--- netbox/ipam/forms/model_forms.py | 114 ++++---- netbox/netbox/forms/base.py | 2 +- netbox/templates/generic/bulk_edit.html | 10 +- netbox/templates/inc/filter_list.html | 8 +- netbox/tenancy/forms/bulk_edit.py | 11 +- netbox/tenancy/forms/filtersets.py | 9 +- netbox/tenancy/forms/model_forms.py | 23 +- netbox/users/forms/bulk_edit.py | 7 +- netbox/users/forms/filtersets.py | 19 +- netbox/users/forms/model_forms.py | 38 ++- netbox/utilities/forms/rendering.py | 9 +- netbox/utilities/templatetags/form_helpers.py | 6 + netbox/virtualization/forms/bulk_edit.py | 23 +- netbox/virtualization/forms/filtersets.py | 42 +-- netbox/virtualization/forms/model_forms.py | 37 ++- netbox/vpn/forms/bulk_edit.py | 27 +- netbox/vpn/forms/filtersets.py | 48 ++-- netbox/vpn/forms/model_forms.py | 70 ++--- netbox/wireless/forms/bulk_edit.py | 11 +- netbox/wireless/forms/filtersets.py | 17 +- netbox/wireless/forms/model_forms.py | 21 +- 35 files changed, 800 insertions(+), 757 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 5c416bff9..3ac311c56 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -8,6 +8,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -34,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): model = Provider fieldsets = ( - (None, ('asns', 'description')), + FieldSet('asns', 'description'), ) nullable_fields = ( 'asns', 'description', 'comments', @@ -56,7 +57,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): model = ProviderAccount fieldsets = ( - (None, ('provider', 'description')), + FieldSet('provider', 'description'), ) nullable_fields = ( 'description', 'comments', @@ -83,7 +84,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): model = ProviderNetwork fieldsets = ( - (None, ('provider', 'service_id', 'description')), + FieldSet('provider', 'service_id', 'description'), ) nullable_fields = ( 'service_id', 'description', 'comments', @@ -103,7 +104,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): model = CircuitType fieldsets = ( - (None, ('color', 'description')), + FieldSet('color', 'description'), ) nullable_fields = ('color', 'description') @@ -164,9 +165,9 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): model = Circuit 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 = ( 'tenant', 'commit_rate', 'description', 'comments', diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 1e1abd068..01445ff6f 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -8,6 +8,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -22,10 +23,10 @@ __all__ = ( class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Provider 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( queryset=Region.objects.all(), @@ -61,8 +62,8 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ProviderAccountFilterForm(NetBoxModelFilterSetForm): model = ProviderAccount fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('provider_id', 'account')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('provider_id', 'account', name=_('Attributes')), ) provider_id = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), @@ -79,8 +80,8 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): model = ProviderNetwork 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( queryset=Provider.objects.all(), @@ -98,8 +99,8 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): class CircuitTypeFilterForm(NetBoxModelFilterSetForm): model = CircuitType fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('color',)), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('color', name=_('Attributes')), ) tag = TagFilterField(model) @@ -112,12 +113,12 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Circuit 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') type_id = DynamicModelMultipleChoiceField( diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index d73da3a02..ee5e47ce7 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm 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 __all__ = ( @@ -30,7 +30,7 @@ class ProviderForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')), + FieldSet('name', 'slug', 'asns', 'description', 'tags'), ) class Meta: @@ -62,7 +62,7 @@ class ProviderNetworkForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')), + FieldSet('provider', 'name', 'service_id', 'description', 'tags'), ) class Meta: @@ -76,9 +76,7 @@ class CircuitTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Circuit Type'), ( - 'name', 'slug', 'color', 'description', 'tags', - )), + FieldSet('name', 'slug', 'color', 'description', 'tags'), ) class Meta: @@ -108,9 +106,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -148,18 +146,15 @@ class CircuitTerminationForm(NetBoxModelForm): ) fieldsets = ( - (_('Circuit Termination'), ( - 'circuit', - 'term_side', - 'description', - 'tags', + FieldSet( + 'circuit', 'term_side', 'description', 'tags', 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: diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index bc2ef8fc9..c1f1fca4d 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -5,6 +5,7 @@ from core.models import * from netbox.forms import NetBoxModelBulkEditForm from netbox.utils import get_data_backend_choices from utilities.forms.fields import CommentField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect __all__ = ( @@ -41,7 +42,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm): model = DataSource fieldsets = ( - (None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')), + FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'), ) nullable_fields = ( 'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules', diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index bd74c0f14..60a3acc44 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -9,7 +9,8 @@ from netbox.forms.mixins import SavedFiltersMixin from netbox.utils import get_data_backend_choices from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm 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__ = ( 'ConfigRevisionFilterForm', @@ -22,8 +23,8 @@ __all__ = ( class DataSourceFilterForm(NetBoxModelFilterSetForm): model = DataSource fieldsets = ( - (None, ('q', 'filter_id')), - (_('Data Source'), ('type', 'status')), + FieldSet('q', 'filter_id'), + FieldSet('type', 'status', name=_('Data Source')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -47,8 +48,8 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm): class DataFileFilterForm(NetBoxModelFilterSetForm): model = DataFile fieldsets = ( - (None, ('q', 'filter_id')), - (_('File'), ('source_id',)), + FieldSet('q', 'filter_id'), + FieldSet('source_id', name=_('File')), ) source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -59,12 +60,12 @@ class DataFileFilterForm(NetBoxModelFilterSetForm): class JobFilterForm(SavedFiltersMixin, FilterForm): 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', - 'started__after', 'completed__before', 'completed__after', 'user', - )), + 'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation') + ), ) object_type = ContentTypeChoiceField( label=_('Object Type'), @@ -125,5 +126,5 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( - (None, ('q', 'filter_id')), + FieldSet('q', 'filter_id'), ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index e0c71fe48..cbca0737a 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -13,6 +13,7 @@ from netbox.registry import registry from netbox.utils import get_data_backend_choices from utilities.forms import get_field_value from utilities.forms.fields import CommentField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect __all__ = ( @@ -49,11 +50,11 @@ class DataSourceForm(NetBoxModelForm): @property def fieldsets(self): 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: fieldsets.append( - (_('Backend Parameters'), self.backend_fields) + FieldSet(*self.backend_fields, name=_('Backend Parameters')) ) return fieldsets @@ -91,8 +92,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm): ) 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: @@ -144,18 +145,24 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass): """ 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', - )), - (_('Config Revision'), ('comment',)) + name=_('Miscellaneous') + ), + FieldSet('comment', name=_('Config Revision')) ) class Meta: diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 79ecc8383..978a5d0a1 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -13,6 +13,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from wireless.models import WirelessLAN, WirelessLANGroup from wireless.choices import WirelessRoleChoices @@ -75,7 +76,7 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm): model = Region fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -94,7 +95,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): model = SiteGroup fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -154,7 +155,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): model = Site fieldsets = ( - (None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')), + FieldSet('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description'), ) nullable_fields = ( 'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments', @@ -194,7 +195,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): model = Location fieldsets = ( - (None, ('site', 'parent', 'status', 'tenant', 'description')), + FieldSet('site', 'parent', 'status', 'tenant', 'description'), ) nullable_fields = ('parent', 'tenant', 'description') @@ -212,7 +213,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): model = RackRole fieldsets = ( - (None, ('color', 'description')), + FieldSet('color', 'description'), ) nullable_fields = ('color', 'description') @@ -341,12 +342,13 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): model = Rack 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', - )), - (_('Weight'), ('weight', 'max_weight', 'weight_unit')), + name=_('Hardware') + ), + FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), ) nullable_fields = ( 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight', @@ -376,7 +378,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): model = RackReservation fieldsets = ( - (None, ('user', 'tenant', 'description')), + FieldSet('user', 'tenant', 'description'), ) nullable_fields = ('comments',) @@ -390,7 +392,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): model = Manufacturer fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -450,11 +452,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): model = DeviceType fieldsets = ( - (_('Device Type'), ( + FieldSet( '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') @@ -489,8 +491,8 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): model = ModuleType 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') @@ -518,7 +520,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): model = DeviceRole fieldsets = ( - (None, ('color', 'vm_role', 'config_template', 'description')), + FieldSet('color', 'vm_role', 'config_template', 'description'), ) nullable_fields = ('color', 'config_template', 'description') @@ -542,7 +544,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): model = Platform fieldsets = ( - (None, ('manufacturer', 'config_template', 'description')), + FieldSet('manufacturer', 'config_template', 'description'), ) nullable_fields = ('manufacturer', 'config_template', 'description') @@ -621,10 +623,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): model = Device 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 = ( 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments', @@ -668,7 +670,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): model = Module fieldsets = ( - (None, ('manufacturer', 'module_type', 'status', 'serial', 'description')), + FieldSet('manufacturer', 'module_type', 'status', 'serial', 'description'), ) nullable_fields = ('serial', 'description', 'comments') @@ -720,8 +722,8 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): model = Cable 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 = ( 'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments', @@ -743,7 +745,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): model = VirtualChassis fieldsets = ( - (None, ('domain', 'description')), + FieldSet('domain', 'description'), ) nullable_fields = ('domain', 'description', 'comments') @@ -791,7 +793,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): model = PowerPanel fieldsets = ( - (None, ('region', 'site_group', 'site', 'location', 'description')), + FieldSet('region', 'site_group', 'site', 'location', 'description'), ) nullable_fields = ('location', 'description', 'comments') @@ -861,8 +863,8 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): model = PowerFeed 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') @@ -1210,7 +1212,7 @@ class ConsolePortBulkEditForm( model = ConsolePort fieldsets = ( - (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')), + FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'), ) nullable_fields = ('module', 'label', 'description') @@ -1227,7 +1229,7 @@ class ConsoleServerPortBulkEditForm( model = ConsoleServerPort fieldsets = ( - (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')), + FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'), ) nullable_fields = ('module', 'label', 'description') @@ -1244,8 +1246,8 @@ class PowerPortBulkEditForm( model = PowerPort 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') @@ -1262,8 +1264,8 @@ class PowerOutletBulkEditForm( model = PowerOutlet 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') @@ -1395,20 +1397,21 @@ class InterfaceBulkEditForm( model = Interface 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', - )), + name=_('Wireless') + ), ) 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): @@ -1488,7 +1491,7 @@ class FrontPortBulkEditForm( model = FrontPort fieldsets = ( - (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), + FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'), ) nullable_fields = ('module', 'label', 'description', 'color') @@ -1505,7 +1508,7 @@ class RearPortBulkEditForm( model = RearPort fieldsets = ( - (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), + FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'), ) nullable_fields = ('module', 'label', 'description', 'color') @@ -1516,7 +1519,7 @@ class ModuleBayBulkEditForm( ): model = ModuleBay fieldsets = ( - (None, ('label', 'position', 'description')), + FieldSet('label', 'position', 'description'), ) nullable_fields = ('label', 'position', 'description') @@ -1527,7 +1530,7 @@ class DeviceBayBulkEditForm( ): model = DeviceBay fieldsets = ( - (None, ('label', 'description')), + FieldSet('label', 'description'), ) nullable_fields = ('label', 'description') @@ -1554,7 +1557,7 @@ class InventoryItemBulkEditForm( model = InventoryItem 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') @@ -1576,7 +1579,7 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): model = InventoryItemRole fieldsets = ( - (None, ('color', 'description')), + FieldSet('color', 'description'), ) nullable_fields = ('color', 'description') @@ -1599,6 +1602,6 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm): ) model = VirtualDeviceContext fieldsets = ( - (None, ('device', 'status', 'tenant')), + FieldSet('device', 'status', 'tenant'), ) nullable_fields = ('device', 'tenant', ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e35055851..4e8e3491c 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,7 +12,8 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice 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 wireless.choices import * @@ -132,8 +133,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Region 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( queryset=Region.objects.all(), @@ -146,8 +147,8 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = SiteGroup 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( queryset=SiteGroup.objects.all(), @@ -160,10 +161,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Site 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') status = forms.MultipleChoiceField( @@ -192,10 +193,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Location 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( queryset=Region.objects.all(), @@ -241,13 +242,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Rack 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') region_id = DynamicModelMultipleChoiceField( @@ -326,13 +327,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte class RackElevationFilterForm(RackFilterForm): 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( queryset=Rack.objects.all(), @@ -348,10 +349,10 @@ class RackElevationFilterForm(RackFilterForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RackReservation 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( queryset=Region.objects.all(), @@ -401,8 +402,8 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Manufacturer 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) @@ -410,14 +411,16 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm): model = DeviceType 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', - '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') manufacturer_id = DynamicModelMultipleChoiceField( @@ -536,13 +539,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm): model = ModuleType 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', - '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') manufacturer_id = DynamicModelMultipleChoiceField( @@ -642,18 +645,20 @@ class DeviceFilterForm( ): model = Device 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', - )), - (_('Miscellaneous'), ( + name=_('Components') + ), + FieldSet( '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') region_id = DynamicModelMultipleChoiceField( @@ -817,9 +822,9 @@ class VirtualDeviceContextFilterForm( ): model = VirtualDeviceContext 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( queryset=Device.objects.all(), @@ -844,8 +849,8 @@ class VirtualDeviceContextFilterForm( class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Module 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( queryset=Manufacturer.objects.all(), @@ -879,9 +884,9 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualChassis 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( queryset=Region.objects.all(), @@ -908,10 +913,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cable 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( queryset=Region.objects.all(), @@ -992,9 +997,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = PowerPanel 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') region_id = DynamicModelMultipleChoiceField( @@ -1031,10 +1036,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = PowerFeed 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( queryset=Region.objects.all(), @@ -1141,11 +1146,11 @@ class PathEndpointFilterForm(CabledFilterForm): class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsolePort 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( label=_('Type'), @@ -1163,11 +1168,11 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsoleServerPort 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( label=_('Type'), @@ -1185,11 +1190,11 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerPort 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( label=_('Type'), @@ -1202,11 +1207,11 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerOutlet 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( label=_('Type'), @@ -1219,14 +1224,14 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = Interface 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') vdc_id = DynamicModelMultipleChoiceField( @@ -1330,11 +1335,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): 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 type = forms.MultipleChoiceField( @@ -1352,11 +1357,11 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): model = RearPort 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( label=_('Type'), @@ -1373,10 +1378,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm): model = ModuleBay 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) position = forms.CharField( @@ -1388,10 +1393,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay 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) @@ -1399,10 +1404,13 @@ class DeviceBayFilterForm(DeviceComponentFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem 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( queryset=InventoryItemRole.objects.all(), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index e0c25dbba..3559aabc6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, 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 virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -78,9 +78,7 @@ class RegionForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Region'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags'), ) class Meta: @@ -99,9 +97,7 @@ class SiteGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Site Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags'), ) class Meta: @@ -136,11 +132,12 @@ class SiteForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Site'), ( + FieldSet( '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: @@ -180,8 +177,8 @@ class LocationForm(TenancyForm, NetBoxModelForm): slug = SlugField() 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: @@ -195,9 +192,7 @@ class RackRoleForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Rack Role'), ( - 'name', 'slug', 'color', 'description', 'tags', - )), + FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')), ) class Meta: @@ -229,19 +224,15 @@ class RackForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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('weight', 'max_weight', 'weight_unit', label=_('Weight')), - 'mounting_depth', - 'desc_units', - )), + 'mounting_depth', 'desc_units', name=_('Dimensions') + ), ) class Meta: @@ -273,8 +264,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -288,9 +279,7 @@ class ManufacturerForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Manufacturer'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')), ) class Meta: @@ -321,12 +310,12 @@ class DeviceTypeForm(NetBoxModelForm): comments = CommentField() 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', - 'weight', 'weight_unit', - )), - (_('Images'), ('front_image', 'rear_image')), + 'weight', 'weight_unit', name=_('Chassis') + ), + FieldSet('front_image', 'rear_image', name=_('Images')), ) class Meta: @@ -354,8 +343,8 @@ class ModuleTypeForm(NetBoxModelForm): comments = CommentField() 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: @@ -374,9 +363,9 @@ class DeviceRoleForm(NetBoxModelForm): slug = SlugField() 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: @@ -403,7 +392,7 @@ class PlatformForm(NetBoxModelForm): ) fieldsets = ( - (_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')), + FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')), ) class Meta: @@ -618,10 +607,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm): ) 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: @@ -675,7 +662,7 @@ class PowerPanelForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - ('Power Panel', ('site', 'location', 'name', 'description', 'tags')), + FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')), ) class Meta: @@ -700,9 +687,12 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -849,7 +839,7 @@ class ModularComponentTemplateForm(ComponentTemplateForm): class ConsolePortTemplateForm(ModularComponentTemplateForm): fieldsets = ( - (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), + FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'), ) class Meta: @@ -861,7 +851,7 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm): class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): fieldsets = ( - (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), + FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'), ) class Meta: @@ -873,9 +863,9 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): class PowerPortTemplateForm(ModularComponentTemplateForm): fieldsets = ( - (None, ( + FieldSet( 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', - )), + ), ) class Meta: @@ -896,7 +886,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm): ) 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: @@ -918,9 +908,11 @@ class InterfaceTemplateForm(ModularComponentTemplateForm): ) 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: @@ -942,10 +934,10 @@ class FrontPortTemplateForm(ModularComponentTemplateForm): ) fieldsets = ( - (None, ( + FieldSet( 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', - )), + ), ) class Meta: @@ -958,7 +950,7 @@ class FrontPortTemplateForm(ModularComponentTemplateForm): class RearPortTemplateForm(ModularComponentTemplateForm): 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: @@ -970,7 +962,7 @@ class RearPortTemplateForm(ModularComponentTemplateForm): class ModuleBayTemplateForm(ComponentTemplateForm): fieldsets = ( - (None, ('device_type', 'name', 'label', 'position', 'description')), + FieldSet('device_type', 'name', 'label', 'position', 'description'), ) class Meta: @@ -982,7 +974,7 @@ class ModuleBayTemplateForm(ComponentTemplateForm): class DeviceBayTemplateForm(ComponentTemplateForm): fieldsets = ( - (None, ('device_type', 'name', 'label', 'description')), + FieldSet('device_type', 'name', 'label', 'description'), ) class Meta: @@ -1023,10 +1015,10 @@ class InventoryItemTemplateForm(ComponentTemplateForm): ) fieldsets = ( - (None, ( + FieldSet( 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 'component_type', 'component_id', - )), + ), ) class Meta: @@ -1069,9 +1061,9 @@ class ModularDeviceComponentForm(DeviceComponentForm): class ConsolePortForm(ModularDeviceComponentForm): fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1082,11 +1074,10 @@ class ConsolePortForm(ModularDeviceComponentForm): class ConsoleServerPortForm(ModularDeviceComponentForm): - fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1097,12 +1088,11 @@ class ConsoleServerPortForm(ModularDeviceComponentForm): class PowerPortForm(ModularDeviceComponentForm): - fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1124,10 +1114,10 @@ class PowerOutletForm(ModularDeviceComponentForm): ) fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1223,15 +1213,18 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): ) 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', - )), + name=_('Wireless') + ), ) class Meta: @@ -1262,10 +1255,10 @@ class FrontPortForm(ModularDeviceComponentForm): ) fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1278,9 +1271,9 @@ class FrontPortForm(ModularDeviceComponentForm): class RearPortForm(ModularDeviceComponentForm): fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1292,7 +1285,7 @@ class RearPortForm(ModularDeviceComponentForm): class ModuleBayForm(DeviceComponentForm): fieldsets = ( - (None, ('device', 'name', 'label', 'position', 'description', 'tags',)), + FieldSet('device', 'name', 'label', 'position', 'description', 'tags',), ) class Meta: @@ -1304,7 +1297,7 @@ class ModuleBayForm(DeviceComponentForm): class DeviceBayForm(DeviceComponentForm): fieldsets = ( - (None, ('device', 'name', 'label', 'description', 'tags',)), + FieldSet('device', 'name', 'label', 'description', 'tags',), ) class Meta: @@ -1412,19 +1405,20 @@ class InventoryItemForm(DeviceComponentForm): ) 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( - (_('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: @@ -1484,9 +1478,7 @@ class InventoryItemRoleForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Inventory Item Role'), ( - 'name', 'slug', 'color', 'description', 'tags', - )), + FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')), ) class Meta: @@ -1522,8 +1514,11 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): ) 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: diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index ea842508f..f811700b4 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.models import * from netbox.forms import NetBoxModelForm from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import APISelect from . import model_forms @@ -113,7 +114,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp # Override fieldsets from FrontPortTemplateForm to omit rear_port_position 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): @@ -274,9 +275,9 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm): # Override fieldsets from FrontPortForm to omit rear_port_position fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta(model_forms.FrontPortForm.Meta): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 73751872f..d4235c465 100644 --- a/netbox/extras/forms/filtersets.py +++ b/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 ( ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import APISelectMultiple, DateTimePicker from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -36,11 +37,11 @@ __all__ = ( class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): 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', - 'ui_editable', 'is_cloneable', - )), + 'ui_editable', 'is_cloneable', name=_('Attributes') + ), ) related_object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), @@ -93,8 +94,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( - (None, ('q', 'filter_id')), - (_('Choices'), ('base_choices', 'choice')), + FieldSet('q', 'filter_id'), + FieldSet('base_choices', 'choice', name=_('Choices')), ) base_choices = forms.MultipleChoiceField( choices=CustomFieldChoiceSetBaseChoices, @@ -107,8 +108,8 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): 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( label=_('Object types'), @@ -137,9 +138,9 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): 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( queryset=DataSource.objects.all(), @@ -178,8 +179,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): 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( label=_('Object type'), @@ -194,8 +195,8 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): 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( label=_('Object types'), @@ -225,8 +226,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): class WebhookFilterForm(NetBoxModelFilterSetForm): model = Webhook 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( label=_('HTTP content type'), @@ -249,9 +250,9 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) 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( queryset=ObjectType.objects.with_feature('event_rules'), @@ -323,12 +324,12 @@ class TagFilterForm(SavedFiltersMixin, FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): 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( queryset=DataSource.objects.all(), @@ -412,8 +413,8 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): 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( queryset=DataSource.objects.all(), @@ -444,9 +445,9 @@ class LocalConfigContextFilterForm(forms.Form): class JournalEntryFilterForm(NetBoxModelFilterSetForm): model = JournalEntry 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( required=False, @@ -482,9 +483,9 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): model = ObjectChange 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( required=False, diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 4e62b3ab7..680bec1e4 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -17,7 +17,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import FieldSet, ObjectAttribute from utilities.forms.widgets import ChoicesWidget, HTMXSelect from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -55,12 +55,15 @@ class CustomFieldForm(forms.ModelForm): ) fieldsets = ( - (_('Custom Field'), ( + FieldSet( '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: @@ -129,8 +132,11 @@ class CustomLinkForm(forms.ModelForm): ) 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: @@ -163,9 +169,9 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): ) 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: @@ -200,8 +206,8 @@ class SavedFilterForm(forms.ModelForm): parameters = JSONField() 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: @@ -232,11 +238,12 @@ class BookmarkForm(forms.ModelForm): class WebhookForm(NetBoxModelForm): 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', - )), - (_('SSL'), ('ssl_verification', 'ca_file_path')), + name=_('HTTP Request') + ), + FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')), ) class Meta: @@ -267,12 +274,13 @@ class EventRuleForm(NetBoxModelForm): ) 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', - )), + name=_('Action') + ), ) class Meta: @@ -361,7 +369,7 @@ class TagForm(forms.ModelForm): ) fieldsets = ( - ('Tag', ('name', 'slug', 'color', 'description', 'object_types')), + FieldSet('name', 'slug', 'color', 'description', 'object_types', name=_('Tag')), ) class Meta: @@ -443,12 +451,13 @@ class ConfigContextForm(SyncedDataMixin, forms.ModelForm): ) 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', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', - )), + name=_('Assignment') + ), ) class Meta: @@ -495,9 +504,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): ) 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: @@ -528,7 +537,7 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm): fieldsets = ( - (None, (ObjectAttribute('parent'), 'name', 'image')), + FieldSet(ObjectAttribute('parent'), 'name', 'image'), ) class Meta: diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 72d57e941..c7f64ab1d 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -13,6 +13,7 @@ from utilities.forms import add_blank_choice from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect from virtualization.models import Cluster, ClusterGroup @@ -55,7 +56,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): model = VRF fieldsets = ( - (None, ('tenant', 'enforce_unique', 'description')), + FieldSet('tenant', 'enforce_unique', 'description'), ) nullable_fields = ('tenant', 'description', 'comments') @@ -75,7 +76,7 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): model = RouteTarget fieldsets = ( - (None, ('tenant', 'description')), + FieldSet('tenant', 'description'), ) nullable_fields = ('tenant', 'description', 'comments') @@ -94,7 +95,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm): model = RIR fieldsets = ( - (None, ('is_private', 'description')), + FieldSet('is_private', 'description'), ) nullable_fields = ('is_private', 'description') @@ -118,7 +119,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm): model = ASNRange fieldsets = ( - (None, ('rir', 'tenant', 'description')), + FieldSet('rir', 'tenant', 'description'), ) nullable_fields = ('description',) @@ -148,7 +149,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): model = ASN fieldsets = ( - (None, ('sites', 'rir', 'tenant', 'description')), + FieldSet('sites', 'rir', 'tenant', 'description'), ) nullable_fields = ('tenant', 'description', 'comments') @@ -177,7 +178,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): model = Aggregate fieldsets = ( - (None, ('rir', 'tenant', 'date_added', 'description')), + FieldSet('rir', 'tenant', 'date_added', 'description'), ) nullable_fields = ('date_added', 'description', 'comments') @@ -195,7 +196,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): model = Role fieldsets = ( - (None, ('weight', 'description')), + FieldSet('weight', 'description'), ) nullable_fields = ('description',) @@ -265,9 +266,9 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): model = Prefix 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 = ( 'site', 'vrf', 'tenant', 'role', 'description', 'comments', @@ -309,7 +310,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): model = IPRange fieldsets = ( - (None, ('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description')), + FieldSet('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description'), ) nullable_fields = ( 'vrf', 'tenant', 'role', 'description', 'comments', @@ -357,8 +358,8 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): model = IPAddress 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 = ( 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments', @@ -400,8 +401,8 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): model = FHRPGroup 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') @@ -485,8 +486,10 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): model = VLANGroup 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',) @@ -556,8 +559,8 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): model = VLAN 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 = ( 'site', 'group', 'tenant', 'role', 'description', 'comments', @@ -587,7 +590,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): model = ServiceTemplate fieldsets = ( - (None, ('protocol', 'ports', 'description')), + FieldSet('protocol', 'ports', 'description'), ) nullable_fields = ('description', 'comments') diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index cf2e4d46e..6610bcaf3 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from virtualization.models import VirtualMachine from vpn.models import L2VPN @@ -42,9 +43,9 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VRF 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( queryset=RouteTarget.objects.all(), @@ -62,9 +63,9 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RouteTarget 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( queryset=VRF.objects.all(), @@ -94,9 +95,9 @@ class RIRFilterForm(NetBoxModelFilterSetForm): class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Aggregate 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( required=False, @@ -114,9 +115,9 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASNRange 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( queryset=RIR.objects.all(), @@ -137,9 +138,9 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN 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( queryset=RIR.objects.all(), @@ -162,11 +163,14 @@ class RoleFilterForm(NetBoxModelFilterSetForm): class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Prefix 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( widget=forms.HiddenInput() @@ -251,9 +255,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPRange 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( required=False, @@ -290,11 +294,14 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPAddress 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') parent = forms.CharField( @@ -364,9 +371,9 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class FHRPGroupFilterForm(NetBoxModelFilterSetForm): model = FHRPGroup 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( label=_('Name'), @@ -396,9 +403,9 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm): 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 region = DynamicModelMultipleChoiceField( @@ -444,10 +451,10 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VLAN 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') region_id = DynamicModelMultipleChoiceField( @@ -504,8 +511,8 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): model = ServiceTemplate fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('protocol', 'port')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('protocol', 'port', name=_('Attributes')), ) protocol = forms.ChoiceField( label=_('Protocol'), @@ -522,9 +529,9 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): class ServiceFilterForm(ServiceTemplateFilterForm): model = Service 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( queryset=Device.objects.all(), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 0aba37fb9..0db9576f1 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, 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 virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -57,9 +57,9 @@ class VRFForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -75,8 +75,8 @@ class VRFForm(TenancyForm, NetBoxModelForm): class RouteTargetForm(TenancyForm, NetBoxModelForm): 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() @@ -91,9 +91,7 @@ class RIRForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('RIR'), ( - 'name', 'slug', 'is_private', 'description', 'tags', - )), + FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')), ) class Meta: @@ -111,8 +109,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -132,8 +130,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm): ) slug = SlugField() 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: @@ -156,8 +154,8 @@ class ASNForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -185,9 +183,7 @@ class RoleForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Role'), ( - 'name', 'slug', 'weight', 'description', 'tags', - )), + FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')), ) class Meta: @@ -227,9 +223,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -254,8 +252,11 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -309,17 +310,17 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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( - (_('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: @@ -458,9 +459,9 @@ class FHRPGroupForm(NetBoxModelForm): comments = CommentField() 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: @@ -518,7 +519,7 @@ class FHRPGroupAssignmentForm(forms.ModelForm): ) fieldsets = ( - (None, (ObjectAttribute('interface'), 'group', 'priority')), + FieldSet(ObjectAttribute('interface'), 'group', 'priority'), ) class Meta: @@ -606,9 +607,12 @@ class VLANGroupForm(NetBoxModelForm): slug = SlugField() 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: @@ -681,9 +685,7 @@ class ServiceTemplateForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Service Template'), ( - 'name', 'protocol', 'ports', 'description', 'tags', - )), + FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Service Template')), ) class Meta: @@ -724,17 +726,15 @@ class ServiceForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Service'), ( + FieldSet( TabbedGroups( - (_('Device'), 'device'), - (_('Virtual Machine'), 'virtual_machine'), + FieldSet('device', name=_('Device')), + FieldSet('virtual_machine', name=_('Virtual Machine')), ), 'name', InlineFields('protocol', 'ports', label=_('Port(s)')), - 'ipaddresses', - 'description', - 'tags', - )), + 'ipaddresses', 'description', 'tags', name=_('Service') + ), ) class Meta: @@ -752,19 +752,17 @@ class ServiceCreateForm(ServiceForm): ) fieldsets = ( - (_('Service'), ( + FieldSet( TabbedGroups( - (_('Device'), 'device'), - (_('Virtual Machine'), 'virtual_machine'), + FieldSet('device', name=_('Device')), + FieldSet('virtual_machine', name=_('Virtual Machine')), ), 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): diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 85064e79d..f63f56ff5 100644 --- a/netbox/netbox/forms/base.py +++ b/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. 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. """ fieldsets = () diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index 10788d62f..ebb0fbc0e 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -49,14 +49,18 @@ Context: {% if form.fieldsets %} {# Render grouped fields according to declared fieldsets #} - {% for group, fields in form.fieldsets %} + {% for fieldset in form.fieldsets %}
- {% if group %}{{ group }}{% else %}{{ model|meta:"verbose_name"|bettertitle }}{% endif %} + {% if fieldset.name %} + {{ fieldset.name }} + {% else %} + {{ model|meta:"verbose_name"|bettertitle }} + {% endif %}
- {% for name in fields %} + {% for name in fieldset.fields %} {% with field=form|getfield:name %} {% if field.name in form.nullable_fields %} {% render_field field bulk_nullable=True %} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index ac87f252d..407add929 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -9,14 +9,14 @@ {{ field }} {% endfor %} {# List filters by group #} - {% for heading, fields in filter_form.fieldsets %} + {% for fieldset in filter_form.fieldsets %}
- {% if heading %} + {% if fieldset.name %}
- {{ heading }} + {{ fieldset.name }}
{% endif %} - {% for name in fields %} + {% for name in fieldset.fields %} {% with field=filter_form|get_item:name %} {% render_field field %} {% endwith %} diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 49866ca3e..5af3f22ac 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -6,6 +6,7 @@ from tenancy.choices import ContactPriorityChoices from tenancy.models import * from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet __all__ = ( 'ContactAssignmentBulkEditForm', @@ -46,7 +47,7 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm): model = Tenant fieldsets = ( - (None, ('group',)), + FieldSet('group'), ) nullable_fields = ('group',) @@ -69,7 +70,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): model = ContactGroup fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -83,7 +84,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): model = ContactRole fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -126,7 +127,7 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): model = Contact 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') @@ -150,6 +151,6 @@ class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm): model = ContactAssignment fieldsets = ( - (None, ('contact', 'role', 'priority')), + FieldSet('contact', 'role', 'priority'), ) nullable_fields = ('priority',) diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index fbd0f2ad0..960ca45b1 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -9,6 +9,7 @@ from tenancy.forms import ContactModelFilterForm from utilities.forms.fields import ( ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) +from utilities.forms.rendering import FieldSet __all__ = ( 'ContactAssignmentFilterForm', @@ -37,8 +38,8 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Tenant 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( queryset=TenantGroup.objects.all(), @@ -82,8 +83,8 @@ class ContactFilterForm(NetBoxModelFilterSetForm): class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): model = ContactAssignment 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( queryset=ObjectType.objects.with_feature('contacts'), diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 7dcb4e433..bc18deed6 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import FieldSet, ObjectAttribute __all__ = ( 'ContactAssignmentForm', @@ -29,9 +29,7 @@ class TenantGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Tenant Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')), ) class Meta: @@ -51,7 +49,7 @@ class TenantForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Tenant'), ('name', 'slug', 'group', 'description', 'tags')), + FieldSet('name', 'slug', 'group', 'description', 'tags', name=_('Tenant')), ) class Meta: @@ -74,9 +72,7 @@ class ContactGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Contact Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')), ) class Meta: @@ -88,9 +84,7 @@ class ContactRoleForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Contact Role'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Contact Role')), ) class Meta: @@ -107,7 +101,10 @@ class ContactForm(NetBoxModelForm): comments = CommentField() 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: @@ -142,7 +139,7 @@ class ContactAssignmentForm(NetBoxModelForm): ) fieldsets = ( - (None, (ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags')), + FieldSet(ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags'), ) class Meta: diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index c56beff14..a26842d09 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -6,6 +6,7 @@ from ipam.formfields import IPNetworkFormField from ipam.validators import prefix_validator from users.models import * from utilities.forms import BulkEditForm +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker __all__ = ( @@ -48,7 +49,7 @@ class UserBulkEditForm(forms.Form): model = User 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') @@ -71,7 +72,7 @@ class ObjectPermissionBulkEditForm(forms.Form): model = ObjectPermission fieldsets = ( - (None, ('enabled', 'description')), + FieldSet('enabled', 'description'), ) nullable_fields = ('description',) @@ -104,7 +105,7 @@ class TokenBulkEditForm(BulkEditForm): model = Token fieldsets = ( - (None, ('write_enabled', 'description', 'expires', 'allowed_ips')), + FieldSet('write_enabled', 'description', 'expires', 'allowed_ips'), ) nullable_fields = ( 'expires', 'description', 'allowed_ips', diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 23bbe45e1..2d5644b98 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -7,6 +7,7 @@ from netbox.forms.mixins import SavedFiltersMixin from users.models import Group, ObjectPermission, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker __all__ = ( @@ -20,16 +21,16 @@ __all__ = ( class GroupFilterForm(NetBoxModelFilterSetForm): model = Group fieldsets = ( - (None, ('q', 'filter_id',)), + FieldSet('q', 'filter_id',), ) class UserFilterForm(NetBoxModelFilterSetForm): model = User 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( queryset=Group.objects.all(), @@ -62,9 +63,9 @@ class UserFilterForm(NetBoxModelFilterSetForm): class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): model = ObjectPermission 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( label=_('Enabled'), @@ -116,8 +117,8 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): class TokenFilterForm(SavedFiltersMixin, FilterForm): model = Token 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( queryset=get_user_model().objects.all(), diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 6c717d1ea..1f199d35c 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -13,6 +13,7 @@ from netbox.preferences import PREFERENCES from users.constants import * from users.models import * from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker from utilities.permissions import qs_filter_from_constraints from utilities.utils import flatten_dict @@ -53,15 +54,10 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass): class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass): 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 pk = forms.MultipleChoiceField( @@ -189,10 +185,10 @@ class UserForm(forms.ModelForm): ) 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: @@ -246,9 +242,9 @@ class GroupForm(forms.ModelForm): ) fieldsets = ( - (None, ('name', )), - (_('Users'), ('users', )), - (_('Permissions'), ('object_permissions', )), + FieldSet('name'), + FieldSet('users', name=_('Users')), + FieldSet('object_permissions', name=_('Permissions')), ) class Meta: @@ -312,11 +308,11 @@ class ObjectPermissionForm(forms.ModelForm): ) 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: diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index ea73c38ff..0d9344131 100644 --- a/netbox/utilities/forms/rendering.py +++ b/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. """ - 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) self.id = ''.join( diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 48a1a5aa8..5365e1c80 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,3 +1,5 @@ +import warnings + from django import template from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups @@ -52,8 +54,12 @@ def render_fieldset(form, fieldset): """ Render a group set of fields. """ + # TODO: Remove in NetBox v4.1 # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3')) if type(fieldset) is not FieldSet: + warnings.warn( + f"{form.__class__} fieldsets contains a non-FieldSet item: {fieldset}" + ) name, fields = fieldset fieldset = FieldSet(*fields, name=name) diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index eaf252824..2bd3434ac 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -10,6 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkRenameForm, add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect from virtualization.choices import * from virtualization.models import * @@ -35,7 +36,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): model = ClusterType fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -49,7 +50,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): model = ClusterGroup fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -104,8 +105,8 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): model = Cluster 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 = ( 'group', 'site', 'tenant', 'description', 'comments', @@ -185,9 +186,9 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): model = VirtualMachine 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 = ( 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments', @@ -262,9 +263,9 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): model = VMInterface 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 = ( 'parent', 'bridge', 'mtu', 'vrf', 'description', @@ -340,7 +341,7 @@ class VirtualDiskBulkEditForm(NetBoxModelBulkEditForm): model = VirtualDisk fieldsets = ( - (None, ('size', 'description')), + FieldSet('size', 'description'), ) nullable_fields = ('description',) diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 5b0d097f8..1cb652a1b 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from virtualization.choices import * from virtualization.models import * from vpn.models import L2VPN @@ -32,19 +33,19 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = ClusterGroup tag = TagFilterField(model) 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): model = Cluster 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') type_id = DynamicModelMultipleChoiceField( @@ -94,12 +95,15 @@ class VirtualMachineFilterForm( ): model = VirtualMachine 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( queryset=ClusterGroup.objects.all(), @@ -185,9 +189,9 @@ class VirtualMachineFilterForm( class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface 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') cluster_id = DynamicModelMultipleChoiceField( @@ -230,9 +234,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): class VirtualDiskFilterForm(NetBoxModelFilterSetForm): model = VirtualDisk 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( queryset=VirtualMachine.objects.all(), diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 186ab8182..bfdfc9ada 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -13,6 +13,7 @@ from utilities.forms import ConfirmationForm from utilities.forms.fields import ( CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect from virtualization.models import * @@ -32,9 +33,7 @@ class ClusterTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Cluster Type'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Type')), ) class Meta: @@ -48,9 +47,7 @@ class ClusterGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Cluster Group'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Group')), ) class Meta: @@ -79,8 +76,8 @@ class ClusterForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -220,12 +217,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -348,11 +345,11 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm): ) 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: @@ -372,7 +369,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm): class VirtualDiskForm(VMComponentForm): fieldsets = ( - (_('Disk'), ('virtual_machine', 'name', 'size', 'description', 'tags')), + FieldSet('virtual_machine', 'name', 'size', 'description', 'tags', name=_('Disk')), ) class Meta: diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py index c3e8eb3ca..a7595a2a7 100644 --- a/netbox/vpn/forms/bulk_edit.py +++ b/netbox/vpn/forms/bulk_edit.py @@ -5,6 +5,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from vpn.choices import * from vpn.models import * @@ -72,9 +73,9 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm): model = Tunnel 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 = ( 'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments', @@ -125,10 +126,10 @@ class IKEProposalBulkEditForm(NetBoxModelBulkEditForm): model = IKEProposal fieldsets = ( - (None, ( + FieldSet( 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', 'description', - )), + ), ) nullable_fields = ( 'sa_lifetime', 'description', 'comments', @@ -159,9 +160,7 @@ class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm): model = IKEPolicy fieldsets = ( - (None, ( - 'version', 'mode', 'preshared_key', 'description', - )), + FieldSet('version', 'mode', 'preshared_key', 'description'), ) nullable_fields = ( 'mode', 'preshared_key', 'description', 'comments', @@ -196,10 +195,10 @@ class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm): model = IPSecProposal fieldsets = ( - (None, ( + FieldSet( 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', 'description', - )), + ), ) nullable_fields = ( 'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments', @@ -221,7 +220,7 @@ class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm): model = IPSecPolicy fieldsets = ( - (None, ('pfs_group', 'description',)), + FieldSet('pfs_group', 'description'), ) nullable_fields = ( 'pfs_group', 'description', 'comments', @@ -253,9 +252,7 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): model = IPSecProfile fieldsets = ( - (_('Profile'), ( - 'mode', 'ike_policy', 'ipsec_policy', 'description', - )), + FieldSet('mode', 'ike_policy', 'ipsec_policy', 'description', name=_('Profile')), ) nullable_fields = ( 'description', 'comments', @@ -282,7 +279,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): model = L2VPN fieldsets = ( - (None, ('type', 'tenant', 'description')), + FieldSet('type', 'tenant', 'description'), ) nullable_fields = ('tenant', 'description', 'comments') diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index a9326c4bc..d25719d06 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -9,6 +9,7 @@ from tenancy.forms import TenancyFilterForm from utilities.forms.fields import ( ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.utils import add_blank_choice from virtualization.models import VirtualMachine from vpn.choices import * @@ -37,10 +38,10 @@ class TunnelGroupFilterForm(NetBoxModelFilterSetForm): class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Tunnel 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( label=_('Status'), @@ -72,8 +73,8 @@ class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): model = TunnelTermination fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Termination'), ('tunnel_id', 'role')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('tunnel_id', 'role', name=_('Termination')), ) tunnel_id = DynamicModelMultipleChoiceField( queryset=Tunnel.objects.all(), @@ -91,8 +92,10 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): class IKEProposalFilterForm(NetBoxModelFilterSetForm): model = IKEProposal 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( label=_('Authentication method'), @@ -120,8 +123,8 @@ class IKEProposalFilterForm(NetBoxModelFilterSetForm): class IKEPolicyFilterForm(NetBoxModelFilterSetForm): model = IKEPolicy 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( label=_('IKE version'), @@ -144,8 +147,8 @@ class IKEPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProposalFilterForm(NetBoxModelFilterSetForm): model = IPSecProposal 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( label=_('Encryption algorithm'), @@ -163,8 +166,8 @@ class IPSecProposalFilterForm(NetBoxModelFilterSetForm): class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): model = IPSecPolicy 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( queryset=IKEProposal.objects.all(), @@ -182,8 +185,8 @@ class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProfileFilterForm(NetBoxModelFilterSetForm): model = IPSecProfile 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( label=_('Mode'), @@ -206,9 +209,9 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm): class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = L2VPN 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( label=_('Type'), @@ -231,10 +234,11 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination 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', - )), + name=_('Assigned Object') + ), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 9674ee2f9..eb2f839d5 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm 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.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -33,7 +33,7 @@ class TunnelGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Tunnel Group'), ('name', 'slug', 'description', 'tags')), + FieldSet('name', 'slug', 'description', 'tags', name=_('Tunnel Group')), ) class Meta: @@ -57,9 +57,9 @@ class TunnelForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -142,17 +142,15 @@ class TunnelCreateForm(TunnelForm): ) 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_outside_ip', - )), - (_('Second Termination'), ( + 'termination1_outside_ip', name=_('First Termination')), + FieldSet( 'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination', - 'termination2_outside_ip', - )), + 'termination2_outside_ip', name=_('Second Termination')), ) def __init__(self, *args, initial=None, **kwargs): @@ -254,7 +252,7 @@ class TunnelTerminationForm(NetBoxModelForm): ) fieldsets = ( - (None, ('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags')), + FieldSet('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags'), ) class Meta: @@ -297,10 +295,11 @@ class TunnelTerminationForm(NetBoxModelForm): class IKEProposalForm(NetBoxModelForm): fieldsets = ( - (_('Proposal'), ('name', 'description', 'tags')), - (_('Parameters'), ( + FieldSet('name', 'description', 'tags', name=_('Proposal')), + FieldSet( 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', - )), + name=_('Parameters') + ), ) class Meta: @@ -318,8 +317,8 @@ class IKEPolicyForm(NetBoxModelForm): ) 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: @@ -332,10 +331,11 @@ class IKEPolicyForm(NetBoxModelForm): class IPSecProposalForm(NetBoxModelForm): fieldsets = ( - (_('Proposal'), ('name', 'description', 'tags')), - (_('Parameters'), ( + FieldSet('name', 'description', 'tags', name=_('Proposal')), + FieldSet( 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', - )), + name=_('Parameters') + ), ) class Meta: @@ -353,8 +353,8 @@ class IPSecPolicyForm(NetBoxModelForm): ) fieldsets = ( - (_('Policy'), ('name', 'description', 'tags')), - (_('Parameters'), ('proposals', 'pfs_group')), + FieldSet('name', 'description', 'tags', name=_('Policy')), + FieldSet('proposals', 'pfs_group', name=_('Parameters')), ) class Meta: @@ -376,8 +376,8 @@ class IPSecProfileForm(NetBoxModelForm): comments = CommentField() 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: @@ -406,9 +406,9 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -446,15 +446,15 @@ class L2VPNTerminationForm(NetBoxModelForm): ) fieldsets = ( - (None, ( + FieldSet( 'l2vpn', TabbedGroups( - (_('VLAN'), 'vlan'), - (_('Device'), 'interface'), - (_('Virtual Machine'), 'vminterface'), + FieldSet('vlan', name=_('VLAN')), + FieldSet('interface', name=_('Device')), + FieldSet('vminterface', name=_('Virtual Machine')), ), 'tags', - )), + ), ) class Meta: diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 43e804345..84916e8d9 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -7,6 +7,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH from wireless.models import * @@ -32,7 +33,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLANGroup fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -86,8 +87,8 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLAN 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 = ( 'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments', @@ -133,8 +134,8 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLink 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 = ( 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments', diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index f4c1cb523..2458d7b48 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -6,6 +6,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import add_blank_choice from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from wireless.choices import * from wireless.models import * @@ -29,10 +30,10 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLAN 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( required=False, @@ -69,10 +70,10 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLink 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( required=False, diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 04e6fce83..05debf8bf 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -6,6 +6,7 @@ from ipam.models import VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.rendering import FieldSet from wireless.models import * __all__ = ( @@ -24,9 +25,7 @@ class WirelessLANGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Wireless LAN Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')), ) class Meta: @@ -51,9 +50,9 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: @@ -158,11 +157,11 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm): comments = CommentField() 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: From d717cb0f07f93962befdae3c897797fe52577c50 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Mar 2024 15:48:17 -0400 Subject: [PATCH 10/44] Use render_fieldset() for bulk edit & filter forms --- netbox/templates/account/preferences.html | 11 ++-------- netbox/templates/generic/bulk_edit.html | 21 +------------------ netbox/templates/inc/filter_list.html | 11 +--------- netbox/utilities/templatetags/form_helpers.py | 8 +++++-- 4 files changed, 10 insertions(+), 41 deletions(-) diff --git a/netbox/templates/account/preferences.html b/netbox/templates/account/preferences.html index 93ca5dfc2..c5a93c162 100644 --- a/netbox/templates/account/preferences.html +++ b/netbox/templates/account/preferences.html @@ -10,15 +10,8 @@ {% csrf_token %} {# Built-in preferences #} - {% for group, fields in form.fieldsets %} -
-
-
{{ group }}
-
- {% for name in fields %} - {% render_field form|getfield:name %} - {% endfor %} -
+ {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} {% endfor %} {# Plugin preferences #} diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index ebb0fbc0e..90b68b25b 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -50,26 +50,7 @@ Context: {# Render grouped fields according to declared fieldsets #} {% for fieldset in form.fieldsets %} -
-
-
- {% if fieldset.name %} - {{ fieldset.name }} - {% else %} - {{ model|meta:"verbose_name"|bettertitle }} - {% endif %} -
-
- {% for name in fieldset.fields %} - {% with field=form|getfield:name %} - {% if field.name in form.nullable_fields %} - {% render_field field bulk_nullable=True %} - {% else %} - {% render_field field %} - {% endif %} - {% endwith %} - {% endfor %} -
+ {% render_fieldset form fieldset %} {% endfor %} {# Render tag add/remove fields #} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index 407add929..b8c93ca4c 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -11,16 +11,7 @@ {# List filters by group #} {% for fieldset in filter_form.fieldsets %}
- {% if fieldset.name %} -
- {{ fieldset.name }} -
- {% endif %} - {% for name in fieldset.fields %} - {% with field=filter_form|get_item:name %} - {% render_field field %} - {% endwith %} - {% endfor %} + {% render_fieldset filter_form fieldset %}
{% empty %} {# List all non-customfield filters as declared in the form class #} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 5365e1c80..723c5206a 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -101,8 +101,12 @@ def render_fieldset(form, fieldset): # A single form field elif item in form.fields: + field = form[item] + # Annotate nullability for bulk editing + if field.name in getattr(form, 'nullable_fields', []): + field._nullable = True rows.append( - ('field', None, [form[item]]) + ('field', None, [field]) ) return { @@ -119,7 +123,7 @@ def render_field(field, bulk_nullable=False, label=None): return { 'field': field, 'label': label or field.label, - 'bulk_nullable': bulk_nullable, + 'bulk_nullable': bulk_nullable or getattr(field, '_nullable', False), } From 85972ab4bfd1da5628b5543d574f77a57070da81 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 08:50:42 -0400 Subject: [PATCH 11/44] Add form rendering utilities to plugins dev docs --- docs/development/internationalization.md | 5 +-- docs/plugins/development/forms.md | 39 ++++++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index bebc97470..df0176b89 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -62,10 +62,11 @@ class Circuit(PrimaryModel): 1. Import `gettext_lazy` as `_`. 2. All form fields must specify a `label` wrapped with `gettext_lazy()`. -3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`. +3. The name of each FieldSet on a form must be wrapped with `gettext_lazy()`. ```python from django.utils.translation import gettext_lazy as _ +from utilities.forms.rendering import FieldSet class CircuitBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField( @@ -74,7 +75,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): ) fieldsets = ( - (_('Circuit'), ('provider', 'type', 'status', 'description')), + FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')), ) ``` diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index 31751855e..332544df7 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -15,16 +15,18 @@ NetBox provides several base form classes for use by plugins. This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields. -| Attribute | Description | -|-------------|-------------------------------------------------------------| -| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | +| Attribute | Description | +|-------------|---------------------------------------------------------------------------------------| +| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) | **Example** ```python +from django.utils.translation import gettext_lazy as _ from dcim.models import Site from netbox.forms import NetBoxModelForm from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet from .models import MyModel class MyModelForm(NetBoxModelForm): @@ -33,8 +35,8 @@ class MyModelForm(NetBoxModelForm): ) comments = CommentField() fieldsets = ( - ('Model Stuff', ('name', 'status', 'site', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), + FieldSet('name', 'status', 'site', 'tags', name=_('Model Stuff')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) class Meta: @@ -52,6 +54,7 @@ This form facilitates the bulk import of new objects from CSV, JSON, or YAML dat **Example** ```python +from django.utils.translation import gettext_lazy as _ from dcim.models import Site from netbox.forms import NetBoxModelImportForm from utilities.forms import CSVModelChoiceField @@ -62,7 +65,7 @@ class MyModelImportForm(NetBoxModelImportForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Assigned site' + help_text=_('Assigned site') ) class Meta: @@ -77,16 +80,18 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi | Attribute | Description | |-------------------|---------------------------------------------------------------------------------------------| | `model` | The model of object being edited | -| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | +| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) | | `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) | **Example** ```python from django import forms +from django.utils.translation import gettext_lazy as _ from dcim.models import Site from netbox.forms import NetBoxModelImportForm from utilities.forms import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet from .models import MyModel, MyModelStatusChoices @@ -106,7 +111,7 @@ class MyModelEditForm(NetBoxModelImportForm): model = MyModel fieldsets = ( - ('Model Stuff', ('name', 'status', 'site')), + FieldSet('name', 'status', 'site', name=_('Model Stuff')), ) nullable_fields = ('site', 'comments') ``` @@ -115,10 +120,10 @@ class MyModelEditForm(NetBoxModelImportForm): This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set. -| Attribute | Description | -|-------------------|-------------------------------------------------------------| -| `model` | The model of object being edited | -| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | +| Attribute | Description | +|-------------|---------------------------------------------------------------------------------------| +| `model` | The model of object being edited | +| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) | **Example** @@ -206,3 +211,13 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c ::: utilities.forms.fields.CSVMultipleContentTypeField options: members: false + +## Form Rendering + +::: utilities.forms.rendering.FieldSet + +::: utilities.forms.rendering.InlineFields + +::: utilities.forms.rendering.TabbedGroups + +::: utilities.forms.rendering.ObjectAttribute From 8d2d0f0bc8d69e619c33a03e2a88c28cafb90dd2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 09:20:49 -0400 Subject: [PATCH 12/44] Misc cleanup & documentation for FieldSets --- netbox/utilities/forms/rendering.py | 39 +++++++++++++++---- netbox/utilities/templatetags/form_helpers.py | 2 +- netbox/vpn/forms/filtersets.py | 2 +- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index 0d9344131..723e911e6 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -12,17 +12,31 @@ __all__ = ( class FieldSet: """ - A generic grouping of fields, with an optional name. Each field will be rendered - on its own row under the heading (name). + A generic grouping of fields, with an optional name. Each item will be rendered + on its own row under the provided heading (name), if any. The following types + may be passed as items: + + * Field name (string) + * InlineFields instance + * TabbedGroups instance + * ObjectAttribute instance + + Parameters: + items: An iterable of items to be rendered (one per row) + name: The fieldset's name, displayed as a heading (optional) """ - def __init__(self, *fields, name=None): - self.fields = fields + def __init__(self, *items, name=None): + self.items = items self.name = name class InlineFields: """ - A set of fields rendered inline (side-by-side) with a shared label; typically nested within a FieldSet. + A set of fields rendered inline (side-by-side) with a shared label. + + Parameters: + fields: An iterable of form field names + label: The label text to render for the row (optional) """ def __init__(self, *fields, label=None): self.fields = fields @@ -31,7 +45,11 @@ class InlineFields: 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 toggle. + + Parameters: + fieldsets: An iterable of FieldSet instances, one per tab. Each FieldSet *must* have a + name assigned, which will be employed as the tab's label. """ def __init__(self, *fieldsets): for fs in fieldsets: @@ -50,14 +68,19 @@ class TabbedGroups: { 'id': f'{self.id}_{i}', 'title': group.name, - 'fields': group.fields, + 'fields': group.items, } for i, group in enumerate(self.groups, start=1) ] class ObjectAttribute: """ - Renders the value for a specific attribute on the form's instance. + Renders the value for a specific attribute on the form's instance. This may be used to + display a read-only value and convey additional context to the user. If the attribute has + a `get_absolute_url()` method, it will be rendered as a hyperlink. + + Parameters: + name: The name of the attribute to be displayed """ def __init__(self, name): self.name = name diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 723c5206a..e9edfed31 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -64,7 +64,7 @@ def render_fieldset(form, fieldset): fieldset = FieldSet(*fields, name=name) rows = [] - for item in fieldset.fields: + for item in fieldset.items: # Multiple fields side-by-side if type(item) is InlineFields: diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index d25719d06..10dc441e2 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -234,7 +234,7 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - FieldSet('filter_id', 'l2vpn_id',), + FieldSet('filter_id', 'l2vpn_id'), FieldSet( 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id', name=_('Assigned Object') From 01ff314acad4a4439298dff9ec8dc5176cc0e745 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 14:06:24 -0400 Subject: [PATCH 13/44] Fixes #15340: Fix flicker on page load with dark mode enabled (#15475) --- netbox/project-static/js/setmode.js | 5 +++-- netbox/templates/base/base.html | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/netbox/project-static/js/setmode.js b/netbox/project-static/js/setmode.js index 8441a542f..ff1c5366b 100644 --- a/netbox/project-static/js/setmode.js +++ b/netbox/project-static/js/setmode.js @@ -5,10 +5,11 @@ * @param inferred {boolean} Value is inferred from browser/system preference. */ function setMode(mode, inferred) { - document.documentElement.setAttribute("data-netbox-color-mode", mode); + document.documentElement.setAttribute("data-bs-theme", mode); localStorage.setItem("netbox-color-mode", mode); localStorage.setItem("netbox-color-mode-inferred", inferred); } + /** * Determine the best initial color mode to use prior to rendering. */ @@ -69,4 +70,4 @@ function initMode() { console.error(error); } return setMode("light", true); -}; +} diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index 1c58047ef..bb35cd3bf 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -9,13 +9,7 @@ data-netbox-url-name="{{ request.resolver_match.url_name }}" data-netbox-base-path="{{ settings.BASE_PATH }}" {% with preferences|get_key:'ui.colormode' as color_mode %} - {% if color_mode == 'dark'%} - data-netbox-color-mode="dark" - {% elif color_mode == 'light' %} - data-netbox-color-mode="light" - {% else %} - data-netbox-color-mode="unset" - {% endif %} + data-netbox-color-mode="{{ color_mode|default:"unset" }}" {% endwith %} > @@ -25,7 +19,16 @@ {# Page title #} {% block title %}{% trans "Home" %}{% endblock %} | NetBox + {# Initialize color mode #} + @@ -53,13 +56,9 @@ {# Additional content #} {% block head %}{% endblock %} - - + + {# Page layout #} {% block layout %}{% endblock %} From 8b74ddfec34dbaf18ba423a5861d4447c811fa89 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 14:18:15 -0400 Subject: [PATCH 14/44] Update release notes --- docs/release-notes/version-4.0.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 60b3115f0..b5889f8cd 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -6,6 +6,7 @@ * The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.) * The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade. +* The `parent` and `parent_id` filters for locations now return only immediate children of the specified location. (Use `ancestor` and `ancestor_id` to return _all_ descendants.) ### New Features @@ -17,18 +18,26 @@ The NetBox user interface has been completely refreshed and updated. The REST API now supports specifying which fields to include in the response data. +#### Advanced FieldSet Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739)) + +New resources have been introduced to enable advanced form rendering without a need for custom HTML templates. + ### Enhancements * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 * [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields +* [#13918](https://github.com/netbox-community/netbox/issues/13918) - Add `facility` field to Location model * [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection +* [#14454](https://github.com/netbox-community/netbox/issues/14454) - Include member devices for virtual chassis in REST API * [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0 * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI * [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects * [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets +* [#15237](https://github.com/netbox-community/netbox/issues/15237) - Ensure consistent filtering ability for all model fields * [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations +* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters) ### Other Changes @@ -44,6 +53,7 @@ The REST API now supports specifying which fields to include in the response dat * [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class +* [#15193](https://github.com/netbox-community/netbox/issues/15193) - Switch to compiled distribution of the `psycopg` library * [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names * [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6) From 4178fcd4653612e0aa6051044bf75c0117ab5dae Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Mar 2024 08:26:04 -0400 Subject: [PATCH 15/44] Closes #15339: Consume entire viewport (#15480) * Closes #15339: Consume entire viewport, except for object detail views * Use fluid containers for all views --- netbox/templates/base/layout.html | 6 +++--- netbox/templates/core/rq_task_list.html | 2 +- netbox/templates/core/rq_worker_list.html | 2 +- netbox/templates/extras/script_result.html | 2 +- netbox/templates/generic/_base.html | 4 ++-- netbox/templates/generic/bulk_delete.html | 2 +- netbox/templates/generic/bulk_remove.html | 8 +++++--- netbox/templates/generic/object.html | 2 +- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index fff12c1e8..071396575 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -41,7 +41,7 @@ Blocks: {# Top menu #}