1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Adopted a different approach to importing related objects

This commit is contained in:
Jeremy Stretch
2019-09-27 16:51:12 -04:00
parent 36d4f0d259
commit edc1b52f65
5 changed files with 167 additions and 178 deletions

View File

@ -24,8 +24,7 @@ from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, MultiObjectField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
from .constants import * from .constants import *
@ -829,126 +828,11 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
} }
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
def clean_device_type(self):
data = self.cleaned_data['device_type']
# Limit fields referencing other components to the parent DeviceType
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and not field_name == 'device_type':
field.queryset = field.queryset.filter(device_type=data)
return data
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsolePortTemplate
fields = [
'name',
]
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'name',
]
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerPortTemplate
fields = [
'name', 'maximum_draw', 'allocated_draw',
]
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = PowerOutletTemplate
fields = [
'name', 'power_port', 'feed_leg',
]
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = InterfaceTemplate
fields = [
'name', 'type', 'mgmt_only',
]
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
queryset=RearPortTemplate.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = FrontPortTemplate
fields = [
'name', 'type', 'rear_port', 'rear_port_position',
]
class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = RearPortTemplate
fields = [
'name', 'type', 'positions',
]
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm): class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name' to_field_name='name'
) )
console_ports = MultiObjectField(
form=ConsolePortTemplateImportForm,
required=False
)
console_server_ports = MultiObjectField(
form=ConsoleServerPortTemplateImportForm,
required=False
)
power_ports = MultiObjectField(
form=PowerPortTemplateImportForm,
required=False
)
power_outlets = MultiObjectField(
form=PowerOutletTemplateImportForm,
required=False
)
interfaces = MultiObjectField(
form=InterfaceTemplateImportForm,
required=False
)
rear_ports = MultiObjectField(
form=RearPortTemplateImportForm,
required=False
)
front_ports = MultiObjectField(
form=FrontPortTemplateImportForm,
required=False
)
class Meta: class Meta:
model = DeviceType model = DeviceType
@ -956,24 +840,6 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
] ]
def save(self, commit=True):
instance = super().save(commit)
if commit:
# Save related components
for field_name, field in self.fields.items():
if isinstance(field, MultiObjectField):
for data in self.cleaned_data[field_name]:
form = field.form(data)
if form.is_valid():
component = form.save(commit=False)
component.device_type = instance
component.save()
return instance
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
@ -1334,6 +1200,116 @@ class DeviceBayTemplateCreateForm(ComponentForm):
) )
#
# Component template import forms
#
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
def __init__(self, device_type, data=None, *args, **kwargs):
# Must pass the parent DeviceType on form initialization
data.update({
'device_type': device_type.pk,
})
print(data)
super().__init__(data, *args, **kwargs)
def clean_device_type(self):
data = self.cleaned_data['device_type']
# Limit fields referencing other components to the parent DeviceType
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
field.queryset = field.queryset.filter(device_type=data)
return data
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name',
]
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name',
]
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'maximum_draw', 'allocated_draw',
]
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'power_port', 'feed_leg',
]
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'type', 'mgmt_only',
]
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
queryset=RearPortTemplate.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
]
class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'name', 'type', 'positions',
]
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name',
]
# #
# Device roles # Device roles
# #

View File

@ -13,22 +13,22 @@ DEVICETYPE_DATA = {
'model': 'TEST-1000', 'model': 'TEST-1000',
'slug': 'test-1000', 'slug': 'test-1000',
'u_height': 2, 'u_height': 2,
'console_ports': [ 'console-ports': [
{'name': 'Console Port 1'}, {'name': 'Console Port 1'},
{'name': 'Console Port 2'}, {'name': 'Console Port 2'},
{'name': 'Console Port 3'}, {'name': 'Console Port 3'},
], ],
'console_server_ports': [ 'console-server-ports': [
{'name': 'Console Server Port 1'}, {'name': 'Console Server Port 1'},
{'name': 'Console Server Port 2'}, {'name': 'Console Server Port 2'},
{'name': 'Console Server Port 3'}, {'name': 'Console Server Port 3'},
], ],
'power_ports': [ 'power-ports': [
{'name': 'Power Port 1'}, {'name': 'Power Port 1'},
{'name': 'Power Port 2'}, {'name': 'Power Port 2'},
{'name': 'Power Port 3'}, {'name': 'Power Port 3'},
], ],
'power_outlets': [ 'power-outlets': [
{'name': 'Power Outlet 1', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A}, {'name': 'Power Outlet 1', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A},
{'name': 'Power Outlet 2', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A}, {'name': 'Power Outlet 2', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A},
{'name': 'Power Outlet 3', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A}, {'name': 'Power Outlet 3', 'power_port': 'Power Port 1', 'feed_leg': POWERFEED_LEG_A},
@ -38,16 +38,21 @@ DEVICETYPE_DATA = {
{'name': 'Interface 2', 'type': IFACE_TYPE_1GE_FIXED}, {'name': 'Interface 2', 'type': IFACE_TYPE_1GE_FIXED},
{'name': 'Interface 3', 'type': IFACE_TYPE_1GE_FIXED}, {'name': 'Interface 3', 'type': IFACE_TYPE_1GE_FIXED},
], ],
'rear_ports': [ 'rear-ports': [
{'name': 'Rear Port 1', 'type': PORT_TYPE_8P8C}, {'name': 'Rear Port 1', 'type': PORT_TYPE_8P8C},
{'name': 'Rear Port 2', 'type': PORT_TYPE_8P8C}, {'name': 'Rear Port 2', 'type': PORT_TYPE_8P8C},
{'name': 'Rear Port 3', 'type': PORT_TYPE_8P8C}, {'name': 'Rear Port 3', 'type': PORT_TYPE_8P8C},
], ],
'front_ports': [ 'front-ports': [
{'name': 'Front Port 1', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 1'}, {'name': 'Front Port 1', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 1'},
{'name': 'Front Port 2', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 2'}, {'name': 'Front Port 2', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 2'},
{'name': 'Front Port 3', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 3'}, {'name': 'Front Port 3', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 3'},
] ],
'device-bays': [
{'name': 'Device Bay 1'},
{'name': 'Device Bay 2'},
{'name': 'Device Bay 3'},
],
} }
@ -67,6 +72,7 @@ class DeviceTypeImportTestCase(TestCase):
dt = DeviceType.objects.get(model='TEST-1000') dt = DeviceType.objects.get(model='TEST-1000')
# Verify all of the components were created # Verify all of the components were created
# TODO: The creation of components now occurs in the view rather than the form
self.assertEqual(dt.consoleport_templates.count(), 3) self.assertEqual(dt.consoleport_templates.count(), 3)
cp1 = ConsolePortTemplate.objects.first() cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1') self.assertEqual(cp1.name, 'Console Port 1')
@ -101,6 +107,10 @@ class DeviceTypeImportTestCase(TestCase):
self.assertEqual(fp1.rear_port, rp1) self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1) self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(dt.devicebay_templates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1')
class DeviceTestCase(TestCase): class DeviceTestCase(TestCase):

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
import re import re
from django.conf import settings from django.conf import settings
@ -13,7 +14,6 @@ from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import slugify
from django.views.generic import View from django.views.generic import View
from circuits.models import Circuit from circuits.models import Circuit
@ -660,6 +660,16 @@ class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
permission_required = 'dcim.add_devicetype' permission_required = 'dcim.add_devicetype'
model = DeviceType model = DeviceType
model_form = forms.DeviceTypeImportForm model_form = forms.DeviceTypeImportForm
related_object_forms = OrderedDict((
('console-ports', forms.ConsolePortTemplateImportForm),
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
('power-ports', forms.PowerPortTemplateImportForm),
('power-outlets', forms.PowerOutletTemplateImportForm),
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm),
))
default_return_url = 'dcim:devicetype_import' default_return_url = 'dcim:devicetype_import'

View File

@ -556,39 +556,6 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source self.widget.attrs['slug-source'] = slug_source
class MultiObjectField(forms.Field):
"""
Use this field to relay data to another form for validation. Useful when importing data via JSON/YAML.
"""
def __init__(self, form, *args, **kwargs):
self.form = form
super().__init__(*args, **kwargs)
def clean(self, value):
# Value needs to be an iterable
if value is None:
return list()
for i, obj_data in enumerate(value, start=1):
# Bind object data to form
form = self.form(obj_data)
# Assign default values for required fields that have not been defined
for field_name, field in form.fields.items():
if field_name not in obj_data and hasattr(field, 'initial'):
form.data[field_name] = field.initial
if not form.is_valid():
errors = [
"Object {} {}: {}".format(i, field, errors) for field, errors in form.errors.items()
]
raise forms.ValidationError(errors)
return value
class FilterChoiceIterator(forms.models.ModelChoiceIterator): class FilterChoiceIterator(forms.models.ModelChoiceIterator):
def __iter__(self): def __iter__(self):

View File

@ -26,6 +26,7 @@ from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset from extras.querysets import CustomFieldQueryset
from utilities.exceptions import AbortTransaction
from utilities.forms import BootstrapMixin, CSVDataField from utilities.forms import BootstrapMixin, CSVDataField
from utilities.utils import csv_format from utilities.utils import csv_format
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
@ -402,6 +403,7 @@ class ObjectImportView(GetReturnURLMixin, View):
""" """
model = None model = None
model_form = None model_form = None
related_object_forms = dict()
template_name = 'utilities/obj_import.html' template_name = 'utilities/obj_import.html'
def create_object(self, data): def create_object(self, data):
@ -436,8 +438,32 @@ class ObjectImportView(GetReturnURLMixin, View):
if model_form.is_valid(): if model_form.is_valid():
with transaction.atomic(): try:
obj = model_form.save() with transaction.atomic():
# Save the primary object
obj = model_form.save()
# Iterate through the related object forms (if any), validating and saving each instance.
for field, related_object_form in self.related_object_forms.items():
for i, rel_obj_data in enumerate(data.get(field, list())):
f = related_object_form(obj, rel_obj_data)
if f.is_valid():
f.save()
else:
# Replicate errors on the related object form to the primary form for display
for field_name, errors in f.errors.items():
for err in errors:
err_msg = "{}[{}] {}: {}".format(field, i, field_name, err)
model_form.add_error(None, err_msg)
raise AbortTransaction()
except AbortTransaction:
pass
if not model_form.errors:
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format( messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
obj.get_absolute_url(), obj obj.get_absolute_url(), obj