Enable dynamic form field rendering

This commit is contained in:
Jeremy Stretch 2025-03-26 15:11:12 -04:00
parent a67ea1305e
commit 5f707320ea
2 changed files with 63 additions and 12 deletions

View File

@ -18,6 +18,7 @@ from utilities.forms.fields import (
) )
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm from .common import InterfaceCommonForm, ModuleCommonForm
@ -423,10 +424,11 @@ class ModuleTypeProfileForm(NetBoxModelForm):
class ModuleTypeForm(NetBoxModelForm): class ModuleTypeForm(NetBoxModelForm):
profile = DynamicModelChoiceField( profile = forms.ModelChoiceField(
label=_('Profile'),
queryset=ModuleTypeProfile.objects.all(), queryset=ModuleTypeProfile.objects.all(),
required=False label=_('Profile'),
required=False,
widget=HTMXSelect()
) )
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'), label=_('Manufacturer'),
@ -434,19 +436,70 @@ class ModuleTypeForm(NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = ( @property
FieldSet('profile', 'manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), def fieldsets(self):
FieldSet('attribute_data', name=_('Profile Attributes')), return [
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')), FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
) FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'))
]
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'attribute_data', 'comments', 'tags', 'comments', 'tags',
] ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Track profile-specific attribute fields
self.attr_fields = []
# Retrieve assigned ModuleTypeProfile, if any
if not (profile_id := get_field_value(self, 'profile')):
return
if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
return
# Extend form with fields for profile attributes
for attr, form_field in self._get_attr_form_fields(profile).items():
field_name = f'attr_{attr}'
self.attr_fields.append(field_name)
self.fields[field_name] = form_field
if self.instance.attribute_data:
self.fields[field_name].initial = self.instance.attribute_data.get(attr)
@staticmethod
def _get_attr_form_fields(profile):
"""
Return a dictionary mapping of attribute names to form fields, suitable for extending
the form per the selected ModuleTypeProfile.
"""
if not profile.schema:
return {}
properties = profile.schema.get('properties', {})
required_fields = profile.schema.get('required', [])
attr_fields = {}
for name, options in properties.items():
prop = JSONSchemaProperty(**options)
attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
return attr_fields
def _post_clean(self):
# Compile attribute data from the individual form fields
self.instance.attribute_data = {
name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
for name in self.attr_fields if self.cleaned_data[name] is not None
}
return super()._post_clean()
class DeviceRoleForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm):
config_template = DynamicModelChoiceField( config_template = DynamicModelChoiceField(

View File

@ -143,9 +143,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
try: try:
jsonschema.validate(self.attribute_data, schema=self.profile.schema) jsonschema.validate(self.attribute_data, schema=self.profile.schema)
except JSONValidationError as e: except JSONValidationError as e:
raise ValidationError({ raise ValidationError(_("Invalid schema: {error}").format(error=e))
'attributes': _("Invalid schema: {error}").format(error=e)
})
else: else:
self.attribute_data = None self.attribute_data = None