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

1import json 

2 

3from django import forms 

4from django.contrib.contenttypes.models import ContentType 

5from django.db.models.fields.related import ManyToManyRel 

6 

7from extras.choices import * 

8from utilities.forms.fields import CommentField, SlugField 

9from utilities.forms.mixins import CheckLastUpdatedMixin 

10 

11from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, TagsMixin 

12 

13__all__ = ( 

14 'NestedGroupModelForm', 

15 'NetBoxModelForm', 

16 'OrganizationalModelForm', 

17 'PrimaryModelForm', 

18) 

19 

20 

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. 

30 

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 = () 

36 

37 def _get_content_type(self): 

38 return ContentType.objects.get_for_model(self._meta.model) 

39 

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 

49 

50 return customfield.to_form_field() 

51 

52 def clean(self): 

53 

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) 

61 

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) 

69 

70 return super().clean() 

71 

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 = {} 

79 

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 ] 

88 

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) 

104 

105 return super()._post_clean() 

106 

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) 

117 

118 

119class PrimaryModelForm(OwnerMixin, NetBoxModelForm): 

120 """ 

121 Form for models which inherit from PrimaryModel. 

122 """ 

123 comments = CommentField() 

124 

125 

126class OrganizationalModelForm(OwnerMixin, NetBoxModelForm): 

127 """ 

128 Form for models which inherit from OrganizationalModel. 

129 """ 

130 slug = SlugField() 

131 comments = CommentField() 

132 

133 

134class NestedGroupModelForm(OwnerMixin, NetBoxModelForm): 

135 """ 

136 Form for models which inherit from NestedGroupModel. 

137 """ 

138 slug = SlugField() 

139 comments = CommentField()