Coverage for netbox/forms/model_forms.py: 29%
61 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import json
3from django import forms
4from django.contrib.contenttypes.models import ContentType
5from django.db.models.fields.related import ManyToManyRel
7from extras.choices import *
8from utilities.forms.fields import CommentField, SlugField
9from utilities.forms.mixins import CheckLastUpdatedMixin
11from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, TagsMixin
13__all__ = (
14 'NestedGroupModelForm',
15 'NetBoxModelForm',
16 'OrganizationalModelForm',
17 'PrimaryModelForm',
18)
21class NetBoxModelForm(
22 ChangelogMessageMixin,
23 CheckLastUpdatedMixin,
24 CustomFieldsMixin,
25 TagsMixin,
26 forms.ModelForm
27):
28 """
29 Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
31 Attributes:
32 fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
33 the rendered form (optional). If not defined, the all fields will be rendered as a single section.
34 """
35 fieldsets = ()
37 def _get_content_type(self):
38 return ContentType.objects.get_for_model(self._meta.model)
40 def _get_form_field(self, customfield):
41 if self.instance.pk:
42 form_field = customfield.to_form_field(set_initial=False)
43 initial = self.instance.custom_field_data.get(customfield.name)
44 if customfield.type == CustomFieldTypeChoices.TYPE_JSON:
45 form_field.initial = json.dumps(initial)
46 else:
47 form_field.initial = initial
48 return form_field
50 return customfield.to_form_field()
52 def clean(self):
54 # Save custom field data on instance
55 for cf_name, customfield in self.custom_fields.items():
56 if cf_name not in self.fields:
57 # Custom fields may be absent when performing bulk updates via import
58 continue
59 key = cf_name[3:] # Strip "cf_" from field name
60 value = self.cleaned_data.get(cf_name)
62 # Convert "empty" values to null
63 if value in self.fields[cf_name].empty_values:
64 self.instance.custom_field_data[key] = None
65 else:
66 if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
67 value = json.loads(value)
68 self.instance.custom_field_data[key] = customfield.serialize(value)
70 return super().clean()
72 def _post_clean(self):
73 """
74 Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
75 Handles both forward and reverse M2M relationships, and supports both simple (single field)
76 and add/remove (dual field) modes.
77 """
78 self.instance._m2m_values = {}
80 # Collect names to process: local M2M fields (includes TaggableManager from django-taggit)
81 # plus reverse M2M relations (ManyToManyRel).
82 names = [field.name for field in self.instance._meta.local_many_to_many]
83 names += [
84 field.get_accessor_name()
85 for field in self.instance._meta.get_fields()
86 if isinstance(field, ManyToManyRel)
87 ]
89 for name in names:
90 if name in self.cleaned_data:
91 # Simple mode: single multi-select field
92 self.instance._m2m_values[name] = list(self.cleaned_data[name])
93 elif f'add_{name}' in self.cleaned_data or f'remove_{name}' in self.cleaned_data:
94 # Add/remove mode: compute the effective set
95 current = set(getattr(self.instance, name).values_list('pk', flat=True)) \
96 if self.instance.pk else set()
97 add_values = set(
98 v.pk for v in self.cleaned_data.get(f'add_{name}', [])
99 )
100 remove_values = set(
101 v.pk for v in self.cleaned_data.get(f'remove_{name}', [])
102 )
103 self.instance._m2m_values[name] = list((current | add_values) - remove_values)
105 return super()._post_clean()
107 def _save_m2m(self):
108 """
109 Save many-to-many field values that were computed in _post_clean(). This handles M2M fields
110 not included in Meta.fields (e.g. those managed via M2MAddRemoveFields).
111 """
112 super()._save_m2m()
113 meta_fields = self._meta.fields
114 for field_name, values in self.instance._m2m_values.items():
115 if not meta_fields or field_name not in meta_fields:
116 getattr(self.instance, field_name).set(values)
119class PrimaryModelForm(OwnerMixin, NetBoxModelForm):
120 """
121 Form for models which inherit from PrimaryModel.
122 """
123 comments = CommentField()
126class OrganizationalModelForm(OwnerMixin, NetBoxModelForm):
127 """
128 Form for models which inherit from OrganizationalModel.
129 """
130 slug = SlugField()
131 comments = CommentField()
134class NestedGroupModelForm(OwnerMixin, NetBoxModelForm):
135 """
136 Form for models which inherit from NestedGroupModel.
137 """
138 slug = SlugField()
139 comments = CommentField()