Просмотр исходного кода

Adopted a different approach to importing related objects

Jeremy Stretch 6 лет назад
Родитель
Сommit
edc1b52f65
5 измененных файлов с 167 добавлено и 178 удалено
  1. 111 135
      netbox/dcim/forms.py
  2. 17 7
      netbox/dcim/tests/test_forms.py
  3. 11 1
      netbox/dcim/views.py
  4. 0 33
      netbox/utilities/forms.py
  5. 28 2
      netbox/utilities/views.py

+ 111 - 135
netbox/dcim/forms.py

@@ -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,
-    BOOLEAN_WITH_BLANK_CHOICES,
+    SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, 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
 #
 #

+ 17 - 7
netbox/dcim/tests/test_forms.py

@@ -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):
 
 

+ 11 - 1
netbox/dcim/views.py

@@ -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'
 
 
 
 

+ 0 - 33
netbox/utilities/forms.py

@@ -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):

+ 28 - 2
netbox/utilities/views.py

@@ -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():
-                    obj = model_form.save()
+                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(
                 messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
                     obj.get_absolute_url(), obj
                     obj.get_absolute_url(), obj