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

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 django.urls import path
 
 
-from dcim.views import CableCreateView, PathTraceView
+from dcim.views import CableEditView, PathTraceView
 from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
 from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
 from . import views
 from . import views
 from .models import *
 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>/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>/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/<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 circuits.models import Circuit, CircuitTermination, Provider
 from dcim.models import *
 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>/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>/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/<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'),
     path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
 
 
     # Console server ports
     # 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>/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>/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/<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'),
     path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
 
 
     # Power ports
     # 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>/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>/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/<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'),
     path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
 
 
     # Power outlets
     # 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>/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>/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/<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'),
     path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
 
 
     # Interfaces
     # Interfaces
@@ -358,7 +358,7 @@ urlpatterns = [
     path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
     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>/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/<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'),
     path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
 
 
     # Front ports
     # 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>/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>/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/<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'),
     # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
 
 
     # Rear ports
     # 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>/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>/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/<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'),
     path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
 
 
     # Module bays
     # 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>/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>/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/<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.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
 
 
-from circuits.models import Circuit
+from circuits.models import Circuit, CircuitTermination
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
 from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
 from netbox.views import generic
 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.paginator import EnhancedPaginator, get_paginate_count
 from utilities.permissions import get_permission_for_model
 from utilities.permissions import get_permission_for_model
 from utilities.utils import count_related
 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()
     queryset = Cable.objects.all()
-    template_name = 'dcim/cable_connect.html'
+    template_name = 'dcim/cable_edit.html'
 
 
     def dispatch(self, request, *args, **kwargs):
     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)
         return super().dispatch(request, *args, **kwargs)
 
 
     def get_object(self, **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):
 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 %}
 {% 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>Site</td>
       <td>{{ terminations.0.device.site|linkify }}</td>
       <td>{{ terminations.0.device.site|linkify }}</td>
     </tr>
     </tr>
+    <tr>
+      <td>Rack</td>
+      <td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
+    </tr>
     <tr>
     <tr>
       <td>Device</td>
       <td>Device</td>
       <td>{{ terminations.0.device|linkify }}</td>
       <td>{{ terminations.0.device|linkify }}</td>
     </tr>
     </tr>
     <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>
     <tr>
     <tr>
       <td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
       <td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>