mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Enable dynamic form field rendering
This commit is contained in:
parent
a67ea1305e
commit
5f707320ea
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user