Coverage for netbox/forms/bulk_import.py: 29%

59 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django import forms 

2from django.core.exceptions import NON_FIELD_ERRORS, ValidationError 

3from django.db import models 

4from django.utils.translation import gettext_lazy as _ 

5 

6from extras.choices import * 

7from extras.models import CustomField, Tag 

8from users.models import Owner 

9from utilities.forms import CSVModelForm 

10from utilities.forms.fields import CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField 

11 

12from .model_forms import NetBoxModelForm 

13 

14__all__ = ( 

15 'NestedGroupModelImportForm', 

16 'NetBoxModelImportForm', 

17 'OrganizationalModelImportForm', 

18 'OwnerCSVMixin', 

19 'PrimaryModelImportForm' 

20) 

21 

22 

23class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): 

24 """ 

25 Base form for creating NetBox objects from CSV data. Used for bulk importing. 

26 """ 

27 tags = CSVModelMultipleChoiceField( 

28 label=_('Tags'), 

29 queryset=Tag.objects.all(), 

30 required=False, 

31 to_field_name='slug', 

32 help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")') 

33 ) 

34 

35 def _get_custom_fields(self, content_type): 

36 # Return only custom fields that are editable in the UI 

37 return [ 

38 cf for cf in CustomField.objects.get_for_model(content_type.model_class()) 

39 if cf.ui_editable == CustomFieldUIEditableChoices.YES 

40 ] 

41 

42 def _get_form_field(self, customfield): 

43 return customfield.to_form_field(for_csv_import=True) 

44 

45 def clean(self): 

46 """ 

47 Cleans data in a form, ensuring proper handling of model fields with `null=True`. 

48 Overrides the `clean` method from the parent form to process and sanitize cleaned 

49 data for defined fields in the associated model. 

50 """ 

51 super().clean() 

52 cleaned = self.cleaned_data 

53 

54 model = getattr(self._meta, "model", None) 

55 if not model: 

56 return cleaned 

57 

58 for f in model._meta.get_fields(): 

59 # Only forward, DB-backed fields (skip M2M & reverse relations) 

60 if not isinstance(f, models.Field) or not f.concrete or f.many_to_many: 

61 continue 

62 

63 if getattr(f, "null", False): 

64 name = f.name 

65 if name not in cleaned: 

66 continue 

67 val = cleaned[name] 

68 # Only coerce empty strings; leave other types alone 

69 if isinstance(val, str) and val.strip() == "": 

70 cleaned[name] = None 

71 

72 return cleaned 

73 

74 def _update_errors(self, errors): 

75 """Convert errors for fields absent from the form to prefixed non-field errors.""" 

76 if hasattr(errors, 'error_dict'): 

77 remapped = [] 

78 passthrough = {} 

79 for field, error_list in errors.error_dict.items(): 

80 if field == NON_FIELD_ERRORS or field in self.fields: 

81 passthrough[field] = error_list 

82 else: 

83 for error in error_list: 

84 message = next(iter(error)) 

85 if error.params: 

86 message = message.replace('%', '%%') 

87 remapped.append(ValidationError( 

88 '{field}: {message}'.format(field=field, message=message), 

89 code=error.code, 

90 params=error.params, 

91 )) 

92 if passthrough: 

93 super()._update_errors(ValidationError(passthrough)) 

94 for error in remapped: 

95 self.add_error(None, error) 

96 else: 

97 super()._update_errors(errors) 

98 

99 

100class OwnerCSVMixin(forms.Form): 

101 owner = CSVModelChoiceField( 

102 queryset=Owner.objects.all(), 

103 required=False, 

104 to_field_name='name', 

105 help_text=_("Name of the object's owner") 

106 ) 

107 

108 

109class PrimaryModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): 

110 """ 

111 Bulk import form for models which inherit from PrimaryModel. 

112 """ 

113 pass 

114 

115 

116class OrganizationalModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): 

117 """ 

118 Bulk import form for models which inherit from OrganizationalModel. 

119 """ 

120 slug = SlugField() 

121 

122 

123class NestedGroupModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): 

124 """ 

125 Bulk import form for models which inherit from NestedGroupModel. 

126 """ 

127 slug = SlugField()