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,
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, MultiObjectField,
BOOLEAN_WITH_BLANK_CHOICES,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup
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):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
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:
model = DeviceType
@ -956,24 +840,6 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
'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):
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
#

View File

@ -13,22 +13,22 @@ DEVICETYPE_DATA = {
'model': 'TEST-1000',
'slug': 'test-1000',
'u_height': 2,
'console_ports': [
'console-ports': [
{'name': 'Console Port 1'},
{'name': 'Console Port 2'},
{'name': 'Console Port 3'},
],
'console_server_ports': [
'console-server-ports': [
{'name': 'Console Server Port 1'},
{'name': 'Console Server Port 2'},
{'name': 'Console Server Port 3'},
],
'power_ports': [
'power-ports': [
{'name': 'Power Port 1'},
{'name': 'Power Port 2'},
{'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 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},
@ -38,16 +38,21 @@ DEVICETYPE_DATA = {
{'name': 'Interface 2', '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 2', '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 2', 'type': PORT_TYPE_8P8C, 'rear_port': 'Rear Port 2'},
{'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')
# 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)
cp1 = ConsolePortTemplate.objects.first()
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_position, 1)
self.assertEqual(dt.devicebay_templates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1')
class DeviceTestCase(TestCase):

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
import re
from django.conf import settings
@ -13,7 +14,6 @@ from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.utils.text import slugify
from django.views.generic import View
from circuits.models import Circuit
@ -660,6 +660,16 @@ class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
permission_required = 'dcim.add_devicetype'
model = DeviceType
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'

View File

@ -556,39 +556,6 @@ class SlugField(forms.SlugField):
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):
def __iter__(self):

View File

@ -26,6 +26,7 @@ from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset
from utilities.exceptions import AbortTransaction
from utilities.forms import BootstrapMixin, CSVDataField
from utilities.utils import csv_format
from .error_handlers import handle_protectederror
@ -402,6 +403,7 @@ class ObjectImportView(GetReturnURLMixin, View):
"""
model = None
model_form = None
related_object_forms = dict()
template_name = 'utilities/obj_import.html'
def create_object(self, data):
@ -436,9 +438,33 @@ class ObjectImportView(GetReturnURLMixin, View):
if model_form.is_valid():
try:
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(
obj.get_absolute_url(), obj
)))