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

Merge cable creation/edit views & forms

jeremystretch 3 лет назад
Родитель
Сommit
7b5ff4c1a5

+ 2 - 2
netbox/circuits/urls.py

@@ -1,6 +1,6 @@
 from django.urls import path
 
-from dcim.views import CableCreateView, PathTraceView
+from dcim.views import CableEditView, PathTraceView
 from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
 from . import views
 from .models import *
@@ -61,6 +61,6 @@ urlpatterns = [
     path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
     path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
     path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
-    path('circuit-terminations/connect/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
+    path('circuit-terminations/connect/', CableEditView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
 
 ]

+ 140 - 303
netbox/dcim/forms/connections.py

@@ -1,305 +1,142 @@
+from django import forms
+
 from circuits.models import Circuit, CircuitTermination, Provider
 from dcim.models import *
-from netbox.forms import NetBoxModelForm
-from tenancy.forms import TenancyForm
-from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
-
-__all__ = (
-    'ConnectCableToCircuitTerminationForm',
-    'ConnectCableToConsolePortForm',
-    'ConnectCableToConsoleServerPortForm',
-    'ConnectCableToFrontPortForm',
-    'ConnectCableToInterfaceForm',
-    'ConnectCableToPowerFeedForm',
-    'ConnectCableToPowerPortForm',
-    'ConnectCableToPowerOutletForm',
-    'ConnectCableToRearPortForm',
-)
-
-
-class BaseCableConnectionForm(TenancyForm, NetBoxModelForm):
-    a_terminations = DynamicModelMultipleChoiceField(
-        queryset=Interface.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied'
-    )
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=Interface.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied'
-    )
-
-    def save(self, *args, **kwargs):
-
-        # Set the A/B terminations on the Cable instance
-        self.instance.a_terminations = self.cleaned_data['a_terminations']
-        self.instance.b_terminations = self.cleaned_data['b_terminations']
-
-        return super().save(*args, **kwargs)
-
-
-class ConnectCableToDeviceForm(BaseCableConnectionForm):
-    """
-    Base form for connecting a Cable to a Device component
-    """
-    termination_b_region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        label='Region',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        label='Site group',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        label='Site',
-        required=False,
-        query_params={
-            'region_id': '$termination_b_region',
-            'group_id': '$termination_b_sitegroup',
-        }
-    )
-    termination_b_location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        label='Location',
-        required=False,
-        null_option='None',
-        query_params={
-            'site_id': '$termination_b_site'
-        }
-    )
-    termination_b_rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        label='Rack',
-        required=False,
-        null_option='None',
-        query_params={
-            'site_id': '$termination_b_site',
-            'location_id': '$termination_b_location',
-        }
-    )
-    termination_b_device = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        label='Device',
-        required=False,
-        query_params={
-            'site_id': '$termination_b_site',
-            'location_id': '$termination_b_location',
-            'rack_id': '$termination_b_rack',
-        }
-    )
-
-    class Meta:
-        model = Cable
-        fields = [
-            'a_terminations', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
-            'termination_b_rack', 'termination_b_device', 'b_terminations', 'type', 'status', 'tenant_group',
-            'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
-        ]
-        widgets = {
-            'status': StaticSelect,
-            'type': StaticSelect,
-            'length_unit': StaticSelect,
-        }
-
-
-class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=ConsolePort.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=ConsoleServerPort.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=PowerPort.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=PowerOutlet.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=Interface.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device',
-            'kind': 'physical',
-        }
-    )
-
-
-class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=FrontPort.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=RearPort.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'device_id': '$termination_b_device'
-        }
-    )
-
-
-class ConnectCableToCircuitTerminationForm(BaseCableConnectionForm):
-    termination_b_provider = DynamicModelChoiceField(
-        queryset=Provider.objects.all(),
-        label='Provider',
-        required=False
-    )
-    termination_b_region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        label='Region',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        label='Site group',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        label='Site',
-        required=False,
-        query_params={
-            'region_id': '$termination_b_region',
-            'group_id': '$termination_b_sitegroup',
-        }
-    )
-    termination_b_circuit = DynamicModelChoiceField(
-        queryset=Circuit.objects.all(),
-        label='Circuit',
-        query_params={
-            'provider_id': '$termination_b_provider',
-            'site_id': '$termination_b_site',
-        }
-    )
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=CircuitTermination.objects.all(),
-        label='Side',
-        disabled_indicator='_occupied',
-        query_params={
-            'circuit_id': '$termination_b_circuit'
-        }
-    )
-
-    class Meta(ConnectCableToDeviceForm.Meta):
-        fields = [
-            'a_terminations', 'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup',
-            'termination_b_site', 'termination_b_circuit', 'b_terminations', 'type', 'status', 'tenant_group',
-            'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
-        ]
-
-
-class ConnectCableToPowerFeedForm(BaseCableConnectionForm):
-    termination_b_region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        label='Region',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        label='Site group',
-        required=False,
-        initial_params={
-            'sites': '$termination_b_site'
-        }
-    )
-    termination_b_site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        label='Site',
-        required=False,
-        query_params={
-            'region_id': '$termination_b_region',
-            'group_id': '$termination_b_sitegroup',
-        }
-    )
-    termination_b_location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        label='Location',
-        required=False,
-        query_params={
-            'site_id': '$termination_b_site'
-        }
-    )
-    termination_b_powerpanel = DynamicModelChoiceField(
-        queryset=PowerPanel.objects.all(),
-        label='Power Panel',
-        required=False,
-        query_params={
-            'site_id': '$termination_b_site',
-            'location_id': '$termination_b_location',
-        }
-    )
-    b_terminations = DynamicModelMultipleChoiceField(
-        queryset=PowerFeed.objects.all(),
-        label='Name',
-        disabled_indicator='_occupied',
-        query_params={
-            'power_panel_id': '$termination_b_powerpanel'
-        }
-    )
-
-    class Meta(ConnectCableToDeviceForm.Meta):
-        fields = [
-            'a_terminations', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
-            'termination_b_location', 'termination_b_powerpanel', 'b_terminations', 'type', 'status', 'tenant_group',
-            'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
-        ]
+from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from .models import CableForm
+
+
+def get_cable_form(a_type, b_type):
+
+    class FormMetaclass(forms.models.ModelFormMetaclass):
+
+        def __new__(mcs, name, bases, attrs):
+
+            for cable_end, term_cls in (('a', a_type), ('b', b_type)):
+
+                attrs[f'termination_{cable_end}_region'] = DynamicModelChoiceField(
+                    queryset=Region.objects.all(),
+                    label='Region',
+                    required=False,
+                    initial_params={
+                        'sites': '$termination_{cable_end}_site'
+                    }
+                )
+                attrs[f'termination_{cable_end}_sitegroup'] = DynamicModelChoiceField(
+                    queryset=SiteGroup.objects.all(),
+                    label='Site group',
+                    required=False,
+                    initial_params={
+                        'sites': '$termination_{cable_end}_site'
+                    }
+                )
+                attrs[f'termination_{cable_end}_site'] = DynamicModelChoiceField(
+                    queryset=Site.objects.all(),
+                    label='Site',
+                    required=False,
+                    query_params={
+                        'region_id': '$termination_{cable_end}_region',
+                        'group_id': '$termination_{cable_end}_sitegroup',
+                    }
+                )
+                attrs[f'termination_{cable_end}_location'] = DynamicModelChoiceField(
+                    queryset=Location.objects.all(),
+                    label='Location',
+                    required=False,
+                    null_option='None',
+                    query_params={
+                        'site_id': '$termination_{cable_end}_site'
+                    }
+                )
+
+                # Device component
+                if hasattr(term_cls, 'device'):
+
+                    attrs[f'termination_{cable_end}_rack'] = DynamicModelChoiceField(
+                        queryset=Rack.objects.all(),
+                        label='Rack',
+                        required=False,
+                        null_option='None',
+                        query_params={
+                            'site_id': '$termination_{cable_end}_site',
+                            'location_id': '$termination_{cable_end}_location',
+                        }
+                    )
+                    attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
+                        queryset=Device.objects.all(),
+                        label='Device',
+                        required=False,
+                        query_params={
+                            'site_id': f'$termination_{cable_end}_site',
+                            'location_id': f'$termination_{cable_end}_location',
+                            'rack_id': f'$termination_{cable_end}_rack',
+                        }
+                    )
+                    attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
+                        queryset=term_cls.objects.all(),
+                        label=term_cls._meta.verbose_name.title(),
+                        disabled_indicator='_occupied',
+                        query_params={
+                            'device_id': f'termination_{cable_end}_device',
+                        }
+                    )
+
+                # PowerFeed
+                elif term_cls == PowerFeed:
+
+                    attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
+                        queryset=PowerPanel.objects.all(),
+                        label='Power Panel',
+                        required=False,
+                        query_params={
+                            'site_id': f'$termination_{cable_end}_site',
+                            'location_id': f'$termination_{cable_end}_location',
+                        }
+                    )
+                    attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
+                        queryset=term_cls.objects.all(),
+                        label='Power Feed',
+                        disabled_indicator='_occupied',
+                        query_params={
+                            'powerpanel_id': f'termination_{cable_end}_powerpanel',
+                        }
+                    )
+
+                # CircuitTermination
+                elif term_cls == CircuitTermination:
+
+                    attrs[f'termination_{cable_end}_provider'] = DynamicModelChoiceField(
+                        queryset=Provider.objects.all(),
+                        label='Provider',
+                        required=False
+                    )
+                    attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
+                        queryset=Circuit.objects.all(),
+                        label='Circuit',
+                        query_params={
+                            'provider_id': f'$termination_{cable_end}_provider',
+                            'site_id': f'$termination_{cable_end}_site',
+                        }
+                    )
+                    attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
+                        queryset=term_cls.objects.all(),
+                        label='Side',
+                        disabled_indicator='_occupied',
+                        query_params={
+                            'circuit_id': f'termination_{cable_end}_circuit',
+                        }
+                    )
+
+            return super().__new__(mcs, name, bases, attrs)
+
+    class _CableForm(CableForm, metaclass=FormMetaclass):
+
+        def save(self, *args, **kwargs):
+
+            # Set the A/B terminations on the Cable instance
+            self.instance.a_terminations = self.cleaned_data['a_terminations']
+            self.instance.b_terminations = self.cleaned_data['b_terminations']
+
+            return super().save(*args, **kwargs)
+
+    return _CableForm

+ 8 - 8
netbox/dcim/urls.py

@@ -294,7 +294,7 @@ urlpatterns = [
     path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
     path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
     path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
-    path('console-ports/connect/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
+    path('console-ports/connect/', views.CableEditView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
     path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
 
     # Console server ports
@@ -310,7 +310,7 @@ urlpatterns = [
     path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
     path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
     path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
-    path('console-server-ports/connect/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
+    path('console-server-ports/connect/', views.CableEditView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
     path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
 
     # Power ports
@@ -326,7 +326,7 @@ urlpatterns = [
     path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
     path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
     path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
-    path('power-ports/connect/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
+    path('power-ports/connect/', views.CableEditView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
     path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
 
     # Power outlets
@@ -342,7 +342,7 @@ urlpatterns = [
     path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
     path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
     path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
-    path('power-outlets/connect/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
+    path('power-outlets/connect/', views.CableEditView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
     path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
 
     # Interfaces
@@ -358,7 +358,7 @@ urlpatterns = [
     path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
     path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
     path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
-    path('interfaces/connect/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
+    path('interfaces/connect/', views.CableEditView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
     path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
 
     # Front ports
@@ -374,7 +374,7 @@ urlpatterns = [
     path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
     path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
     path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
-    path('front-ports/connect/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
+    path('front-ports/connect/', views.CableEditView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
     # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
 
     # Rear ports
@@ -390,7 +390,7 @@ urlpatterns = [
     path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
     path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
     path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
-    path('rear-ports/connect/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
+    path('rear-ports/connect/', views.CableEditView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
     path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
 
     # Module bays
@@ -500,6 +500,6 @@ urlpatterns = [
     path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
     path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
     path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
-    path('power-feeds/connect/', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}),
+    path('power-feeds/connect/', views.CableEditView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}),
 
 ]

+ 35 - 58
netbox/dcim/views.py

@@ -12,12 +12,12 @@ from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 
-from circuits.models import Circuit
+from circuits.models import Circuit, CircuitTermination
 from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
 from netbox.views import generic
-from utilities.forms import ConfirmationForm
+from utilities.forms import ConfirmationForm, restrict_form_fields
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.permissions import get_permission_for_model
 from utilities.utils import count_related
@@ -2804,72 +2804,49 @@ class PathTraceView(generic.ObjectView):
         }
 
 
-class CableCreateView(generic.ObjectEditView):
+class CableEditView(generic.ObjectEditView):
     queryset = Cable.objects.all()
-    template_name = 'dcim/cable_connect.html'
+    template_name = 'dcim/cable_edit.html'
 
     def dispatch(self, request, *args, **kwargs):
 
-        # Set the form class based on the type of component being connected
-        self.form = {
-            'dcim.consoleport': forms.ConnectCableToConsolePortForm,
-            'dcim.consoleserverport': forms.ConnectCableToConsoleServerPortForm,
-            'dcim.powerport': forms.ConnectCableToPowerPortForm,
-            'dcim.poweroutlet': forms.ConnectCableToPowerOutletForm,
-            'dcim.interface': forms.ConnectCableToInterfaceForm,
-            'dcim.frontport': forms.ConnectCableToFrontPortForm,
-            'dcim.rearport': forms.ConnectCableToRearPortForm,
-            'dcim.powerfeed': forms.ConnectCableToPowerFeedForm,
-            'circuits.circuittermination': forms.ConnectCableToCircuitTerminationForm,
-        }[request.GET.get('termination_b_type')]
+        # If creating a new Cable, initialize the form class using URL query params
+        if 'pk' not in kwargs:
+            termination_types = {
+                'dcim.consoleport': ConsolePort,
+                'dcim.consoleserverport': ConsoleServerPort,
+                'dcim.powerport': PowerPort,
+                'dcim.poweroutlet': PowerOutlet,
+                'dcim.interface': Interface,
+                'dcim.frontport': FrontPort,
+                'dcim.rearport': RearPort,
+                'dcim.powerfeed': PowerFeed,
+                'circuits.circuittermination': CircuitTermination,
+            }
+
+            a_type = kwargs.pop('termination_a_type')
+            b_type = termination_types[request.GET.get('termination_b_type')]
+
+            self.form = forms.get_cable_form(a_type, b_type)
 
         return super().dispatch(request, *args, **kwargs)
 
     def get_object(self, **kwargs):
-        # Always return a new instance
-        return self.queryset.model()
-
-    def get(self, request, *args, **kwargs):
-        obj = self.get_object(**kwargs)
-        obj = self.alter_object(obj, request, args, kwargs)
-        initial_data = request.GET
-
-        app_label, model = request.GET.get('termination_b_type').split('.')
-        termination_b_type = ContentType.objects.get(app_label=app_label, model=model)
-
-        # TODO
-        # # Set initial site and rack based on side A termination (if not already set)
-        # termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
-        # if 'termination_b_site' not in initial_data:
-        #     initial_data['termination_b_site'] = termination_a_site
-        # if 'termination_b_rack' not in initial_data:
-        #     initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
-        form = self.form(instance=obj, initial=initial_data)
-
-        # TODO Find a better way to infer the near-end parent object
-        termination_a = kwargs['termination_a_type'].objects.filter(pk=int(initial_data['a_terminations'])).first()
-
-        # Set the queryset of termination A
-        form.fields['a_terminations'].queryset = kwargs['termination_a_type'].objects.all()
-        form.fields['a_terminations'].widget.add_query_params({
-            'device_id': termination_a.device_id,
-        })
-
-        return render(request, self.template_name, {
-            'obj': obj,
-            'obj_type': Cable._meta.verbose_name,
-            'termination_a_type': kwargs['termination_a_type']._meta.model_name,
-            'termination_a_parent': termination_a.parent_object,
-            'termination_b_type': termination_b_type.name,
-            'form': form,
-            'return_url': self.get_return_url(request, obj),
-        })
+        """
+        Hack into get_object() to set the form class when editing an existing Cable, since ObjectEditView
+        doesn't currently provide a hook for dynamic class resolution.
+        """
+        obj = super().get_object(**kwargs)
 
+        if obj.pk:
+            # TODO: Optimize this logic
+            termination_a = obj.terminations.filter(cable_end='A').first()
+            a_type = termination_a.termination._meta.model if termination_a else None
+            termination_b = obj.terminations.filter(cable_end='B').first()
+            b_type = termination_b.termination._meta.model if termination_a else None
+            self.form = forms.get_cable_form(a_type, b_type)
 
-class CableEditView(generic.ObjectEditView):
-    queryset = Cable.objects.all()
-    form = forms.CableForm
-    template_name = 'dcim/cable_edit.html'
+        return obj
 
 
 class CableDeleteView(generic.ObjectDeleteView):

+ 0 - 167
netbox/templates/dcim/cable_connect.html

@@ -1,167 +0,0 @@
-{% extends 'base/layout.html' %}
-{% load static %}
-{% load helpers %}
-{% load form_helpers %}
-
-{% block title %}Connect Cable to {{ termination_b_type|bettertitle }}{% endblock %}
-
-{% block tabs %}
-<ul class="nav nav-tabs px-3">
-  <li class="nav-item" role="presentation">
-    <a href="#" role="tab" data-bs-toggle="tab" class="nav-link active">Connect Cable</a>
-  </li>
-</ul>
-{% endblock %}
-
-{% block content-wrapper %}
-  <div class="tab-content">
-    {% render_errors form %}
-    <form method="post">
-      {% csrf_token %}
-      {% for field in form.hidden_fields %}
-          {{ field }}
-      {% endfor %}
-      <div class="row my-3">
-          <div class="col col-md-5">
-              <div class="card h-100">
-                  <h5 class="card-header offset-sm-3">A Side</h5>
-                  <div class="card-body">
-                      {% if termination_a_type == 'circuit' %}
-                          {# Circuit termination #}
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Provider</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.provider }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Circuit</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.cid }}" disabled />
-                              </div>
-                          </div>
-                      {% else %}
-                          {# Device component #}
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Region</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.site.region }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Site Group</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.site.group }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Site</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.site }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Location</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.location|default:"None" }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Rack</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent.rack|default:"None" }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Device</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_parent }}" disabled />
-                              </div>
-                          </div>
-                          <div class="row mb-3">
-                              <label class="col-sm-3 col-form-label text-lg-end">Type</label>
-                              <div class="col">
-                                  <input class="form-control" value="{{ termination_a_type|capfirst }}" disabled />
-                              </div>
-                          </div>
-                      {% endif %}
-                      {% render_field form.a_terminations %}
-                  </div>
-              </div>
-          </div>
-          <div class="col col-md-2 flex-column justify-content-center align-items-center d-none d-md-flex">
-              <i class="mdi mdi-swap-horizontal-bold mdi-48px"></i>
-          </div>
-          <div class="col col-md-5">
-              <div class="card h-100">
-                  <h5 class="card-header offset-sm-3">B Side</h5>
-                  <div class="card-body">
-                      {% if tabs %}
-                          <ul class="nav nav-tabs">
-                              {% for url, link in tabs %}
-                                  <li class="nav-item" role="presentation">
-                                      <a class="nav-link" href="{{ url }}">{{ link }}</a>
-                                  </li>
-                              {% endfor %}
-                          </ul>
-                      {% endif %}
-                      {% if 'termination_b_provider' in form.fields %}
-                          {% render_field form.termination_b_provider %}
-                      {% endif %}
-                      {% if 'termination_b_region' in form.fields %}
-                          {% render_field form.termination_b_region %}
-                      {% endif %}
-                      {% if 'termination_b_sitegroup' in form.fields %}
-                          {% render_field form.termination_b_sitegroup %}
-                      {% endif %}
-                      {% if 'termination_b_site' in form.fields %}
-                          {% render_field form.termination_b_site %}
-                      {% endif %}
-                      {% if 'termination_b_location' in form.fields %}
-                          {% render_field form.termination_b_location %}
-                      {% endif %}
-                      {% if 'termination_b_rack' in form.fields %}
-                          {% render_field form.termination_b_rack %}
-                      {% endif %}
-                      {% if 'termination_b_device' in form.fields %}
-                          {% render_field form.termination_b_device %}
-                      {% endif %}
-                      {% if 'termination_b_type' in form.fields %}
-                          {% render_field form.termination_b_type %}
-                      {% endif %}
-                      {% if 'termination_b_powerpanel' in form.fields %}
-                          {% render_field form.termination_b_powerpanel %}
-                      {% endif %}
-                      {% if 'termination_b_circuit' in form.fields %}
-                          {% render_field form.termination_b_circuit %}
-                      {% endif %}
-                      <div class="row mb-3">
-                          <label class="col-sm-3 col-form-label text-lg-end">Type</label>
-                          <div class="col">
-                              <input class="form-control" value="{{ termination_b_type|capfirst }}" disabled />
-                          </div>
-                      </div>
-                      {% render_field form.b_terminations %}
-                  </div>
-              </div>
-          </div>
-      </div>
-      <div class="row my-3 justify-content-center">
-        <div class="col col-md-8">
-          <div class="card">
-            <h5 class="card-header offset-sm-3">Cable</h5>
-            <div class="card-body">
-              {% include 'dcim/inc/cable_form.html' %}
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="row my-3">
-        <div class="col col-md-12 text-center">
-          <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
-          <button type="submit" name="_update" class="btn btn-primary">Connect</button>
-        </div>
-      </div>
-    </form>
-  </div>
-{% endblock %}

+ 123 - 3
netbox/templates/dcim/cable_edit.html

@@ -1,5 +1,125 @@
-{% extends 'generic/object_edit.html' %}
+{% extends 'base/layout.html' %}
+{% load static %}
+{% load helpers %}
+{% load form_helpers %}
 
-{% block form %}
-  {% include 'dcim/inc/cable_form.html' %}
+{% block title %}Connect Cable{% endblock %}
+
+{% block tabs %}
+<ul class="nav nav-tabs px-3">
+  <li class="nav-item" role="presentation">
+    <a href="#" role="tab" data-bs-toggle="tab" class="nav-link active">Connect Cable</a>
+  </li>
+</ul>
+{% endblock %}
+
+{% block content-wrapper %}
+  <div class="tab-content">
+    {% render_errors form %}
+    <form method="post">
+      {% csrf_token %}
+      {% for field in form.hidden_fields %}
+          {{ field }}
+      {% endfor %}
+      <div class="row my-3">
+          <div class="col col-md-5">
+              <div class="card h-100">
+                  <h5 class="card-header offset-sm-3">A Side</h5>
+                  <div class="card-body">
+                      {% render_field form.termination_a_region %}
+                      {% render_field form.termination_a_sitegroup %}
+                      {% render_field form.termination_a_site %}
+                      {% render_field form.termination_a_location %}
+                      {% if 'termination_a_rack' in form.fields %}
+                          {% render_field form.termination_a_rack %}
+                      {% endif %}
+                      {% if 'termination_a_device' in form.fields %}
+                          {% render_field form.termination_a_device %}
+                      {% endif %}
+                      {% if 'termination_a_powerpanel' in form.fields %}
+                          {% render_field form.termination_a_powerpanel %}
+                      {% endif %}
+                      {% if 'termination_a_provider' in form.fields %}
+                          {% render_field form.termination_a_provider %}
+                      {% endif %}
+                      {% if 'termination_a_circuit' in form.fields %}
+                          {% render_field form.termination_a_circuit %}
+                      {% endif %}
+                      {% render_field form.a_terminations %}
+                  </div>
+              </div>
+          </div>
+          <div class="col col-md-2 flex-column justify-content-center align-items-center d-none d-md-flex">
+              <i class="mdi mdi-swap-horizontal-bold mdi-48px"></i>
+          </div>
+          <div class="col col-md-5">
+              <div class="card h-100">
+                  <h5 class="card-header offset-sm-3">B Side</h5>
+                  <div class="card-body">
+                      {% render_field form.termination_b_region %}
+                      {% render_field form.termination_b_sitegroup %}
+                      {% render_field form.termination_b_site %}
+                      {% render_field form.termination_b_location %}
+                      {% if 'termination_b_rack' in form.fields %}
+                          {% render_field form.termination_b_rack %}
+                      {% endif %}
+                      {% if 'termination_b_device' in form.fields %}
+                          {% render_field form.termination_b_device %}
+                      {% endif %}
+                      {% if 'termination_b_powerpanel' in form.fields %}
+                          {% render_field form.termination_b_powerpanel %}
+                      {% endif %}
+                      {% if 'termination_b_provider' in form.fields %}
+                          {% render_field form.termination_b_provider %}
+                      {% endif %}
+                      {% if 'termination_b_circuit' in form.fields %}
+                          {% render_field form.termination_b_circuit %}
+                      {% endif %}
+                      {% render_field form.b_terminations %}
+                  </div>
+              </div>
+          </div>
+      </div>
+      <div class="row my-3 justify-content-center">
+        <div class="col col-md-8">
+          <div class="card">
+            <h5 class="card-header offset-sm-3">Cable</h5>
+            <div class="card-body">
+              {% render_field form.status %}
+              {% render_field form.type %}
+              {% render_field form.tenant_group %}
+              {% render_field form.tenant %}
+              {% render_field form.label %}
+              {% render_field form.color %}
+              <div class="row mb-3">
+                <label class="col-sm-3 col-form-label text-lg-end">{{ form.length.label }}</label>
+                <div class="col-md-5">
+                  {{ form.length }}
+                </div>
+                <div class="col-md-4">
+                  {{ form.length_unit }}
+                </div>
+                <div class="invalid-feedback"></div>
+              </div>
+              {% render_field form.tags %}
+              {% if form.custom_fields %}
+                <div class="field-group">
+                  <div class="row mb-3">
+                    <h5 class="offset-sm-3">Custom Fields</h5>
+                  </div>
+                  {% render_custom_fields form %}
+                </div>
+              {% endif %}
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="row my-3">
+        <div class="col col-md-12 text-center">
+          <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
+          <button type="submit" name="_update" class="btn btn-primary">Connect</button>
+        </div>
+      </div>
+    </form>
+  </div>
 {% endblock %}

+ 0 - 27
netbox/templates/dcim/inc/cable_form.html

@@ -1,27 +0,0 @@
-{% load form_helpers %}
-
-{% render_field form.status %}
-{% render_field form.type %}
-{% render_field form.tenant_group %}
-{% render_field form.tenant %}
-{% render_field form.label %}
-{% render_field form.color %}
-<div class="row mb-3">
-    <label class="col-sm-3 col-form-label text-lg-end">{{ form.length.label }}</label>
-    <div class="col-md-5">
-        {{ form.length }}
-    </div>
-    <div class="col-md-4">
-        {{ form.length_unit }}
-    </div>
-    <div class="invalid-feedback"></div>
-</div>
-{% render_field form.tags %}
-{% if form.custom_fields %}
-  <div class="field-group">
-    <div class="row mb-3">
-      <h5 class="offset-sm-3">Custom Fields</h5>
-    </div>
-    {% render_custom_fields form %}
-  </div>
-{% endif %}

+ 20 - 2
netbox/templates/dcim/inc/cable_termination.html

@@ -6,13 +6,31 @@
       <td>Site</td>
       <td>{{ terminations.0.device.site|linkify }}</td>
     </tr>
+    <tr>
+      <td>Rack</td>
+      <td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
+    </tr>
     <tr>
       <td>Device</td>
       <td>{{ terminations.0.device|linkify }}</td>
     </tr>
     <tr>
-      <td>Rack</td>
-      <td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
+      <td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
+      <td>
+        {% for term in terminations %}
+          {{ term|linkify }}{% if not forloop.last %},{% endif %}
+        {% endfor %}
+      </td>
+    </tr>
+  {% elif terminations.0.power_panel %}
+    {# Power feed #}
+    <tr>
+      <td>Site</td>
+      <td>{{ terminations.0.power_panel.site|linkify }}</td>
+    </tr>
+    <tr>
+      <td>Power Panel</td>
+      <td>{{ terminations.0.power_panel|linkify }}</td>
     </tr>
     <tr>
       <td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>