@@ -595,68 +595,118 @@ def __init__(self, *args, **kwargs):
595595 }
596596 )
597597
598- # Determine if IP source fields should be shown based on selected definition
599- show_ip_sources = False
598+ # Determine form mode based on selected definition
599+ # Modes: "simple_ip" (plain ipv4/ipv6), "record" (individual fields per record type), "manual" (text data)
600+ self ._record_mode = False
601+ self ._record_types_list = []
602+ self ._record_ip_index = None
603+ mode = "manual"
600604 is_array = True
601605 definition_id = get_field_value (self , "definition" )
606+ defn = None
602607 if definition_id :
603608 try :
604609 defn = OptionDefinition .objects .get (pk = definition_id )
605- show_ip_sources = defn .option_type == "ipv4-address"
606610 is_array = defn .is_array
611+ if defn .option_type in OptionDefinition .IP_TYPES :
612+ mode = "simple_ip"
613+ elif defn .option_type == "record" and defn .record_types :
614+ mode = "record"
607615 except OptionDefinition .DoesNotExist :
608616 pass
609617
610- if not show_ip_sources :
618+ if mode == "manual" :
611619 self .fields .pop ("ipam_ip_sources" , None )
612620 self .fields .pop ("dns_record_sources" , None )
613- else :
614- # Hide data field — IP sources replace manual data entry for IP-type options
621+ elif mode == "simple_ip" :
622+ # Hide data field — IP sources replace manual data entry
615623 self .fields .pop ("data" , None )
616- # For single-value options, use single-select instead of multi-select
617- if not is_array :
618- self .fields ["ipam_ip_sources" ] = DynamicModelChoiceField (
619- queryset = IPAddress .objects .all (),
620- required = False ,
621- label = "IPAM IP Address" ,
622- help_text = "Select an IP address from NetBox IPAM. Overrides the Data field in KEA config output." ,
623- )
624-
625- # Add DNS Record source field if netbox-dns integration is enabled
626- if get_plugin_config ("netbox_dhcp_kea_plugin" , "enable_netbox_dns" ):
627- try :
628- from netbox_dns .models import Record as DNSRecord
629-
630- dns_query_params = {"type" : ["A" , "AAAA" , "CNAME" ]}
631- if is_array :
632- self .fields ["dns_record_sources" ] = DynamicModelMultipleChoiceField (
633- queryset = DNSRecord .objects .all (),
634- query_params = dns_query_params ,
635- required = False ,
636- label = "DNS Records" ,
637- help_text = "Select DNS A/AAAA or CNAME records. IPs resolved at KEA config generation time." ,
638- )
639- else :
640- self .fields ["dns_record_sources" ] = DynamicModelChoiceField (
641- queryset = DNSRecord .objects .all (),
642- query_params = dns_query_params ,
643- required = False ,
644- label = "DNS Record" ,
645- help_text = "Select a DNS A/AAAA or CNAME record. IP resolved at KEA config generation time." ,
646- )
647- except ImportError :
648- pass
649-
650- # Pre-populate IP source fields for existing objects
624+ self ._add_ip_source_fields (is_array )
651625 if self .instance .pk :
652626 self ._populate_ip_source_fields ()
627+ elif mode == "record" :
628+ self ._record_mode = True
629+ self ._record_types_list = defn .parsed_record_types ()
630+ self ._record_ip_index = defn .record_ip_field_index ()
631+ # Hide the plain data field — we build it from individual record fields
632+ self .fields .pop ("data" , None )
633+ # Add a field for each position in the record
634+ for i , type_name in enumerate (self ._record_types_list ):
635+ if type_name in OptionDefinition .IP_TYPES :
636+ continue # IP positions get source selectors below
637+ if type_name == "boolean" :
638+ self .fields [f"record_field_{ i } " ] = forms .ChoiceField (
639+ choices = [("true" , "True" ), ("false" , "False" )],
640+ label = f"Field { i + 1 } ({ type_name } )" ,
641+ required = False ,
642+ help_text = f"Record field { i + 1 } : { type_name } " ,
643+ )
644+ else :
645+ self .fields [f"record_field_{ i } " ] = forms .CharField (
646+ label = f"Field { i + 1 } ({ type_name } )" ,
647+ required = False ,
648+ help_text = f"Record field { i + 1 } : { type_name } " ,
649+ )
650+ # Add IP source selectors if the record has an IP-type field
651+ if self ._record_ip_index is not None :
652+ # Only the last field in a record can be an array in KEA
653+ ip_is_array = is_array and self ._record_ip_index == len (self ._record_types_list ) - 1
654+ self ._add_ip_source_fields (ip_is_array )
655+ else :
656+ self .fields .pop ("ipam_ip_sources" , None )
657+ self .fields .pop ("dns_record_sources" , None )
658+ # Pre-populate for existing objects
659+ if self .instance .pk :
660+ self ._populate_ip_source_fields ()
661+
662+ def _add_ip_source_fields (self , is_array ):
663+ """Add IPAM and DNS IP source selector fields."""
664+ if not is_array :
665+ self .fields ["ipam_ip_sources" ] = DynamicModelChoiceField (
666+ queryset = IPAddress .objects .all (),
667+ required = False ,
668+ label = "IPAM IP Address" ,
669+ help_text = "Select an IP address from NetBox IPAM." ,
670+ )
671+ # (multi-select is already defined as a class field; only override for single)
672+
673+ if get_plugin_config ("netbox_dhcp_kea_plugin" , "enable_netbox_dns" ):
674+ try :
675+ from netbox_dns .models import Record as DNSRecord
676+
677+ dns_query_params = {"type" : ["A" , "AAAA" , "CNAME" ]}
678+ if is_array :
679+ self .fields ["dns_record_sources" ] = DynamicModelMultipleChoiceField (
680+ queryset = DNSRecord .objects .all (),
681+ query_params = dns_query_params ,
682+ required = False ,
683+ label = "DNS Records" ,
684+ help_text = "Select DNS A/AAAA or CNAME records. IPs resolved at KEA config generation time." ,
685+ )
686+ else :
687+ self .fields ["dns_record_sources" ] = DynamicModelChoiceField (
688+ queryset = DNSRecord .objects .all (),
689+ query_params = dns_query_params ,
690+ required = False ,
691+ label = "DNS Record" ,
692+ help_text = "Select a DNS A/AAAA or CNAME record. IP resolved at KEA config generation time." ,
693+ )
694+ except ImportError :
695+ pass
653696
654697 def _populate_ip_source_fields (self ):
655698 """Set initial values for IP source fields from existing OptionDataIPSource entries."""
656699 from django .contrib .contenttypes .models import ContentType
657700
658701 from netbox_dhcp_kea_plugin .models import OptionDataIPSource
659702
703+ # Populate record manual fields from stored JSON
704+ if self ._record_mode and self .instance .record_manual_fields :
705+ for idx_str , value in self .instance .record_manual_fields .items ():
706+ field_name = f"record_field_{ idx_str } "
707+ if field_name in self .fields :
708+ self .initial [field_name ] = value
709+
660710 sources = OptionDataIPSource .objects .filter (option_data = self .instance ).order_by ("ordinal" )
661711 if not sources .exists ():
662712 return
@@ -687,18 +737,34 @@ def fieldsets(self):
687737 name = "Option Selection" ,
688738 ),
689739 ]
690- # Build IP Sources or manual Data fieldset based on available fields
691- ip_source_items = []
692- if "ipam_ip_sources" in self .fields :
693- ip_source_items .append ("ipam_ip_sources" )
694- if "dns_record_sources" in self .fields :
695- ip_source_items .append ("dns_record_sources" )
696- if ip_source_items :
697- base .append (
698- FieldSet (* ip_source_items , InlineFields ("always_send" , "csv_format" , label = "Flags" ), name = "IP Sources" )
699- )
740+ # Build value fieldset based on form mode
741+ if self ._record_mode :
742+ # Record mode: individual fields for each record position + IP source selectors
743+ record_items = []
744+ for i , type_name in enumerate (self ._record_types_list ):
745+ field_name = f"record_field_{ i } "
746+ if field_name in self .fields :
747+ record_items .append (field_name )
748+ if "ipam_ip_sources" in self .fields :
749+ record_items .append ("ipam_ip_sources" )
750+ if "dns_record_sources" in self .fields :
751+ record_items .append ("dns_record_sources" )
752+ record_items .append (InlineFields ("always_send" , "csv_format" , label = "Flags" ))
753+ base .append (FieldSet (* record_items , name = "Record Fields" ))
700754 else :
701- base .append (FieldSet ("data" , InlineFields ("always_send" , "csv_format" , label = "Flags" ), name = "Value" ))
755+ ip_source_items = []
756+ if "ipam_ip_sources" in self .fields :
757+ ip_source_items .append ("ipam_ip_sources" )
758+ if "dns_record_sources" in self .fields :
759+ ip_source_items .append ("dns_record_sources" )
760+ if ip_source_items :
761+ base .append (
762+ FieldSet (
763+ * ip_source_items , InlineFields ("always_send" , "csv_format" , label = "Flags" ), name = "IP Sources"
764+ )
765+ )
766+ else :
767+ base .append (FieldSet ("data" , InlineFields ("always_send" , "csv_format" , label = "Flags" ), name = "Value" ))
702768 base .append (FieldSet ("description" , "tags" , name = "Metadata" ))
703769 return tuple (base )
704770
@@ -711,6 +777,21 @@ def clean_csv_format(self):
711777 return csv_format
712778
713779 def save (self , * args , ** kwargs ):
780+ # Collect record manual fields into JSON before saving
781+ if self ._record_mode :
782+ manual_fields = {}
783+ for i , type_name in enumerate (self ._record_types_list ):
784+ field_name = f"record_field_{ i } "
785+ if field_name in self .cleaned_data :
786+ manual_fields [str (i )] = self .cleaned_data [field_name ]
787+ self .instance .record_manual_fields = manual_fields or None
788+ if self ._record_ip_index is not None :
789+ # Has IP sources — data will be assembled at config generation time
790+ self .instance .data = ""
791+ else :
792+ # No IP sources — assemble data now from manual fields
793+ parts = [manual_fields .get (str (i ), "" ) for i in range (len (self ._record_types_list ))]
794+ self .instance .data = ", " .join (parts )
714795 instance = super ().save (* args , ** kwargs )
715796 self ._save_ip_sources (instance )
716797 return instance
0 commit comments