content: import json
  
  from django import forms
  from django.contrib.contenttypes.models import ContentType
  from django.db import models
  from django.db.models import Q
  from django.utils.translation import gettext_lazy as _
  
  from core.models import ObjectType
  from extras.choices import *
  from extras.models import CustomField, Tag
  from utilities.forms import BulkEditForm, CSVModelForm
  from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
  from utilities.forms.mixins import CheckLastUpdatedMixin
  from .mixins import ChangelogMessageMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin
  
  __all__ = (
      'NetBoxModelForm',
      'NetBoxModelImportForm',
      'NetBoxModelBulkEditForm',
      'NetBoxModelFilterSetForm',
  )
  
  
  class NetBoxModelForm(ChangelogMessageMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
      """
      Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
  
      Attributes:
          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 = ()
  
      def _get_content_type(self):
          return ContentType.objects.get_for_model(self._meta.model)
  
      def _get_form_field(self, customfield):
          if self.instance.pk:
              form_field = customfield.to_form_field(set_initial=False)
              initial = self.instance.custom_field_data.get(customfield.name)
              if customfield.type == CustomFieldTypeChoices.TYPE_JSON:
                  form_field.initial = json.dumps(initial)
              else:
                  form_field.initial = initial
              return form_field
  
          return customfield.to_form_field()
  
      def clean(self):
  
          # Save custom field data on instance
          for cf_name, customfield in self.custom_fields.items():
              if cf_name not in self.fields:
                  # Custom fields may be absent when performing bulk updates via import
                  continue
              key = cf_name[3:]  # Strip "cf_" from field name
              value = self.cleaned_data.get(cf_name)
  
              # Convert "empty" values to null
              if value in self.fields[cf_name].empty_values:
                  self.instance.custom_field_data[key] = None
              else:
                  if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
                      value = json.loads(value)
                  self.instance.custom_field_data[key] = customfield.serialize(value)
  
          return super().clean()
  
      def _post_clean(self):
          """
          Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
          """
          self.instance._m2m_values = {}
          for field in self.instance._meta.local_many_to_many:
              if field.name in self.cleaned_data:
                  self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
  
          return super()._post_clean()
  
  
  class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
      """
      Base form for creating a NetBox objects from CSV data. Used for bulk importing.
      """
      tags = CSVModelMultipleChoiceField(
          label=_('Tags'),
          queryset=Tag.objects.all(),
          required=False,
          to_field_name='slug',
          help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")')
      )
  
      def _get_custom_fields(self, content_type):
          return CustomField.objects.filter(
              object_types=content_type,
              ui_editable=CustomFieldUIEditableChoices.YES
          )
  
      def _get_form_field(self, customfield):
          return customfield.to_form_field(for_csv_import=True)
  
      def clean(self):
          """
          Cleans data in a form, ensuring proper handling of model fields with `null=True`.
          Overrides the `clean` method from the parent form to process and sanitize cleaned
          data for defined fields in the associated model.
          """
          super().clean()
          cleaned = self.cleaned_data
  
          model = getattr(self._meta, "model", None)
          if not model:
              return cleaned
  
          for f in model._meta.get_fields():
              # Only forward, DB-backed fields (skip M2M & reverse relations)
              if not isinstance(f, models.Field) or not f.concrete or f.many_to_many:
                  continue
  
              if getattr(f, "null", False):
                  name = f.name
                  if name not in cleaned:
                      continue
                  val = cleaned[name]
                  # Only coerce empty strings; leave other types alone
                  if isinstance(val, str) and val.strip() == "":
                      cleaned[name] = None
  
          return cleaned
  
  
  class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, BulkEditForm):
      """
      Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
      fields and adding/removing tags.
  
      Attributes:
          fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
              the rendered form (optional). If not defined, the all fields will be rendered as a single section.
      """
      fieldsets = None
  
      pk = forms.ModelMultipleChoiceField(
          queryset=None,  # Set from self.model on init
          widget=forms.MultipleHiddenInput
      )
      add_tags = DynamicModelMultipleChoiceField(
          label=_('Add tags'),
          queryset=Tag.objects.all(),
          required=False
      )
      remove_tags = DynamicModelMultipleChoiceField(
          label=_('Remove tags'),
          queryset=Tag.objects.all(),
          required=False
      )
  
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
  
          self.fields['pk'].queryset = self.model.objects.all()
  
          # Restrict tag fields by model
          object_type = ObjectType.objects.get_for_model(self.model)
          self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
          self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
  
          self._extend_nullable_fields()
  
      def _get_form_field(self, customfield):
          return customfield.to_form_field(set_initial=False, enforce_required=False)
  
      def _extend_nullable_fields(self):
          nullable_custom_fields = [
              name for name, customfield in self.custom_fields.items()
              if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
          ]
          self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
  
  
  class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
      """
      Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
      corresponding FilterSet *must* provide a `q` filter.
  
      Attributes:
          model: The model class associated with the form
          fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
              the rendered form (optional). If not defined, the all fields will be rendered as a single section.
          selector_fields: An iterable of names of fields to display by default when rendering the form as
              a selector widget
      """
      q = forms.CharField(
          required=False,
          label=_('Search')
      )
  
      selector_fields = ('filter_id', 'q')
  
      def _get_custom_fields(self, content_type):
          return super()._get_custom_fields(content_type).exclude(
              Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
              Q(type=CustomFieldTypeChoices.TYPE_JSON)
          )
  
      def _get_form_field(self, customfield):
          return customfield.to_form_field(
              set_initial=False, enforce_required=False, enforce_visibility=False, for_filterset_form=True
          )
  