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
« 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 _
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
12from .model_forms import NetBoxModelForm
14__all__ = (
15 'NestedGroupModelImportForm',
16 'NetBoxModelImportForm',
17 'OrganizationalModelImportForm',
18 'OwnerCSVMixin',
19 'PrimaryModelImportForm'
20)
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 )
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 ]
42 def _get_form_field(self, customfield):
43 return customfield.to_form_field(for_csv_import=True)
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
54 model = getattr(self._meta, "model", None)
55 if not model:
56 return cleaned
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
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
72 return cleaned
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)
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 )
109class PrimaryModelImportForm(OwnerCSVMixin, NetBoxModelImportForm):
110 """
111 Bulk import form for models which inherit from PrimaryModel.
112 """
113 pass
116class OrganizationalModelImportForm(OwnerCSVMixin, NetBoxModelImportForm):
117 """
118 Bulk import form for models which inherit from OrganizationalModel.
119 """
120 slug = SlugField()
123class NestedGroupModelImportForm(OwnerCSVMixin, NetBoxModelImportForm):
124 """
125 Bulk import form for models which inherit from NestedGroupModel.
126 """
127 slug = SlugField()