Skip to content

Commit ead26d0

Browse files
arthansonjeremystretch
authored andcommitted
4347 initial code for json import
1 parent 0ce7f84 commit ead26d0

File tree

8 files changed

+93
-525
lines changed

8 files changed

+93
-525
lines changed

netbox/dcim/views.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ class DeviceTypeDeleteView(generic.ObjectDeleteView):
936936
queryset = DeviceType.objects.all()
937937

938938

939-
class DeviceTypeImportView(generic.ObjectImportView):
939+
class DeviceTypeImportView(generic.BulkImportView):
940940
additional_permissions = [
941941
'dcim.add_devicetype',
942942
'dcim.add_consoleporttemplate',
@@ -952,6 +952,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
952952
]
953953
queryset = DeviceType.objects.all()
954954
model_form = forms.DeviceTypeImportForm
955+
table = tables.DeviceTypeTable
955956
related_object_forms = {
956957
'console-ports': forms.ConsolePortTemplateImportForm,
957958
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
@@ -1069,7 +1070,7 @@ class ModuleTypeDeleteView(generic.ObjectDeleteView):
10691070
queryset = ModuleType.objects.all()
10701071

10711072

1072-
class ModuleTypeImportView(generic.ObjectImportView):
1073+
class ModuleTypeImportView(generic.BulkImportView):
10731074
additional_permissions = [
10741075
'dcim.add_moduletype',
10751076
'dcim.add_consoleporttemplate',
@@ -1082,6 +1083,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
10821083
]
10831084
queryset = ModuleType.objects.all()
10841085
model_form = forms.ModuleTypeImportForm
1086+
table = tables.ModuleTypeTable
10851087
related_object_forms = {
10861088
'console-ports': forms.ConsolePortTemplateImportForm,
10871089
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,

netbox/extras/tests/test_customfields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,7 @@ def test_import(self):
887887
)
888888
csv_data = '\n'.join(','.join(row) for row in data)
889889

890-
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
890+
response = self.client.post(reverse('dcim:site_import'), {'data': csv_data, 'format': 'csv'})
891891
self.assertEqual(response.status_code, 200)
892892
self.assertEqual(Site.objects.count(), 3)
893893

netbox/netbox/views/generic/bulk_views.py

Lines changed: 31 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from utilities.htmx import is_htmx
2727
from utilities.permissions import get_permission_for_model
2828
from utilities.views import GetReturnURLMixin
29+
from utilities.forms.choices import ImportFormatChoices
2930
from .base import BaseMultiObjectView
3031
from .mixins import ActionsMixin, TableMixin
3132
from .utils import get_prerequisite_model
@@ -288,7 +289,7 @@ def post(self, request):
288289
})
289290

290291

291-
class OldBulkImportView(GetReturnURLMixin, BaseMultiObjectView):
292+
class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
292293
"""
293294
Import objects in bulk (CSV format).
294295
@@ -297,147 +298,14 @@ class OldBulkImportView(GetReturnURLMixin, BaseMultiObjectView):
297298
"""
298299
template_name = 'generic/bulk_import.html'
299300
model_form = None
301+
related_object_forms = dict()
300302

301-
def _import_form(self, *args, **kwargs):
302-
303-
class ImportForm(BootstrapMixin, Form):
304-
csv = CSVDataField(
305-
from_form=self.model_form
306-
)
307-
csv_file = CSVFileField(
308-
label="CSV file",
309-
from_form=self.model_form,
310-
required=False
311-
)
312-
313-
def clean(self):
314-
csv_rows = self.cleaned_data['csv'][1] if 'csv' in self.cleaned_data else None
315-
csv_file = self.files.get('csv_file')
316-
317-
# Check that the user has not submitted both text data and a file
318-
if csv_rows and csv_file:
319-
raise ValidationError(
320-
"Cannot process CSV text and file attachment simultaneously. Please choose only one import "
321-
"method."
322-
)
323-
324-
return ImportForm(*args, **kwargs)
325-
326-
def _create_objects(self, form, request):
327-
new_objs = []
328-
if request.FILES:
329-
headers, records = form.cleaned_data['csv_file']
330-
else:
331-
headers, records = form.cleaned_data['csv']
332-
333-
for row, data in enumerate(records, start=1):
334-
obj_form = self.model_form(data, headers=headers)
335-
restrict_form_fields(obj_form, request.user)
336-
337-
if obj_form.is_valid():
338-
obj = self._save_obj(obj_form, request)
339-
new_objs.append(obj)
340-
else:
341-
for field, err in obj_form.errors.items():
342-
form.add_error('csv', f'Row {row} {field}: {err[0]}')
343-
raise ValidationError("")
344-
345-
return new_objs
346-
347-
def _save_obj(self, obj_form, request):
303+
def prep_related_object_data(self, parent, data):
348304
"""
349-
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
305+
Hook to modify the data for related objects before it's passed to the related object form (for example, to
306+
assign a parent object).
350307
"""
351-
return obj_form.save()
352-
353-
def get_required_permission(self):
354-
return get_permission_for_model(self.queryset.model, 'add')
355-
356-
#
357-
# Request handlers
358-
#
359-
360-
def get(self, request):
361-
362-
return render(request, self.template_name, {
363-
'model': self.model_form._meta.model,
364-
'form': self._import_form(),
365-
'fields': self.model_form().fields,
366-
'return_url': self.get_return_url(request),
367-
**self.get_extra_context(request),
368-
})
369-
370-
def post(self, request):
371-
logger = logging.getLogger('netbox.views.BulkImportView')
372-
form = self._import_form(request.POST, request.FILES)
373-
374-
if form.is_valid():
375-
logger.debug("Form validation was successful")
376-
377-
try:
378-
# Iterate through CSV data and bind each row to a new model form instance.
379-
with transaction.atomic():
380-
new_objs = self._create_objects(form, request)
381-
382-
# Enforce object-level permissions
383-
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
384-
raise PermissionsViolation
385-
386-
# Compile a table containing the imported objects
387-
obj_table = self.table(new_objs)
388-
389-
if new_objs:
390-
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
391-
logger.info(msg)
392-
messages.success(request, msg)
393-
394-
return render(request, "import_success.html", {
395-
'table': obj_table,
396-
'return_url': self.get_return_url(request),
397-
})
398-
399-
except ValidationError:
400-
clear_webhooks.send(sender=self)
401-
402-
except (AbortRequest, PermissionsViolation) as e:
403-
logger.debug(e.message)
404-
form.add_error(None, e.message)
405-
clear_webhooks.send(sender=self)
406-
407-
else:
408-
logger.debug("Form validation failed")
409-
410-
return render(request, self.template_name, {
411-
'model': self.model_form._meta.model,
412-
'form': form,
413-
'fields': self.model_form().fields,
414-
'return_url': self.get_return_url(request),
415-
**self.get_extra_context(request),
416-
})
417-
418-
419-
class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
420-
"""
421-
Import objects in bulk (CSV format).
422-
423-
Attributes:
424-
model_form: The form used to create each imported object
425-
"""
426-
template_name = 'generic/bulk_import.html'
427-
model_form = None
428-
429-
'''
430-
supported_formats = [
431-
{
432-
'name': 'CSV',
433-
'help_text': 'Enter the list of column headers followed by one line per record to be imported, using ' \
434-
'commas to separate values. Multi-line data and values containing commas may be wrapped ' \
435-
'in double quotes.'
436-
},
437-
{'name': 'JSON', },
438-
{'name': 'YAML', },
439-
]
440-
'''
308+
return data
441309

442310
def _create_object(self, request, model_form):
443311

@@ -478,16 +346,16 @@ def _create_object(self, request, model_form):
478346

479347
return obj
480348

481-
def _create_objects(self, form, request):
349+
def _create_objects(self, form, format, data, request):
482350
new_objs = []
483351
for row_num, record in enumerate(data['data'], start=1):
484-
if format == 'csv':
485-
model_form = self.model_form(record, headers=headers)
352+
if format == ImportFormatChoices.CSV:
353+
model_form = self.model_form(record, headers=data['headers'])
486354
else:
487355
model_form = self.model_form(record)
488356
restrict_form_fields(model_form, request.user)
489357

490-
if format == 'json' or format == 'yaml':
358+
if format == ImportFormatChoices.JSON or format == ImportFormatChoices.YAML:
491359
# Assign default values for any fields which were not specified.
492360
# We have to do this manually because passing 'initial=' to the form
493361
# on initialization merely sets default values for the widgets.
@@ -505,8 +373,8 @@ def _create_objects(self, form, request):
505373
# Replicate model form errors for display
506374
for field, errors in model_form.errors.items():
507375
for err in errors:
508-
if format == 'csv':
509-
form.add_error('csv', f'Row {row} {field}: {err[0]}')
376+
if format == ImportFormatChoices.CSV:
377+
form.add_error(None, f'Row {row_num} {field}: {err}')
510378
else:
511379
if field == '__all__':
512380
form.add_error(None, err)
@@ -530,45 +398,50 @@ def get_required_permission(self):
530398
# Request handlers
531399
#
532400

533-
def get_context(self, request, data_form, file_form):
401+
def get_context(self, request, data_form, file_form, form=None):
402+
# small hack - need to return 'form' set to either the file or data form
403+
# as the bulk_import base view relies on it for error reporting.
534404
return {
535405
'model': self.model_form._meta.model,
536406
'data_form': data_form,
407+
'form': form,
537408
'file_form': file_form,
538409
'fields': self.model_form().fields,
539410
'return_url': self.get_return_url(request),
540411
**self.get_extra_context(request),
541412
}
542413

543414
def get(self, request):
544-
data_form = ImportForm()
545-
file_form = FileUploadImportForm()
415+
data_form = ImportForm(related=self.related_object_forms)
416+
file_form = FileUploadImportForm(related=self.related_object_forms)
546417

547418
return render(request, self.template_name, self.get_context(request, data_form, file_form))
548419

549420
def post(self, request):
550421
logger = logging.getLogger('netbox.views.BulkImportView')
551-
data_form = ImportForm(request.POST)
552-
file_form = FileUploadImportForm(request.POST, request.FILES)
422+
data_form = ImportForm(request.POST, related=self.related_object_forms)
423+
file_form = FileUploadImportForm(request.POST, request.FILES, related=self.related_object_forms)
553424

554425
data = None
555-
if 'data_submit' in request.POST:
556-
if data_form.is_valid():
557-
logger.debug("Data Import form validation was successful")
558-
data = data_form.cleaned_data
559-
elif 'file_submit' in request.POST:
426+
form = None
427+
if 'file_submit' in request.POST:
428+
form = file_form
560429
if file_form.is_valid():
561430
logger.debug("File Import form validation was successful")
562431
data = file_form.cleaned_data
432+
else: # data_submit
433+
form = data_form
434+
if data_form.is_valid():
435+
logger.debug("Data Import form validation was successful")
436+
data = data_form.cleaned_data
563437

564438
if data:
565439
format = data['format']
566-
headers = data['headers'] if format == 'csv' else None
567440

568441
try:
569442
# Iterate through data and bind each row to a new model form instance.
570443
with transaction.atomic():
571-
new_objs = self._create_objects(form, request)
444+
new_objs = self._create_objects(form, format, data, request)
572445

573446
# Enforce object-level permissions
574447
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
@@ -598,7 +471,7 @@ def post(self, request):
598471
else:
599472
logger.debug("Form validation failed")
600473

601-
return render(request, self.template_name, self.get_context(request, data_form, file_form))
474+
return render(request, self.template_name, self.get_context(request, data_form, file_form, form))
602475

603476

604477
class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):

0 commit comments

Comments
 (0)