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

Closes #10054: Implement advanced UI controls for object selection (#11952)

* WIP

* WIP

* WIP

* Make object selector functional

* Replace extraneous form fields with selector widgets

* Avoid overlap with filterset field names

* Show checkmarks next to visibile filters

* Update results automatically when searching

* Include selector for device/VM component parent fields

* Use selector for filtering VLAN group/site

* Limit selector to 100 results
Jeremy Stretch 2 лет назад
Родитель
Сommit
d1f76bec37

+ 6 - 34
netbox/circuits/forms/model_forms.py

@@ -1,7 +1,7 @@
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from circuits.models import *
 from circuits.models import *
-from dcim.models import Region, Site, SiteGroup
+from dcim.models import Site
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
@@ -114,50 +114,22 @@ class CircuitTerminationForm(NetBoxModelForm):
             'provider_id': '$provider',
             'provider_id': '$provider',
         },
         },
     )
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        },
-        required=False
-    )
-    provider_network_provider = DynamicModelChoiceField(
-        queryset=Provider.objects.all(),
         required=False,
         required=False,
-        label='Provider',
-        initial_params={
-            'networks': 'provider_network'
-        }
+        selector=True
     )
     )
     provider_network = DynamicModelChoiceField(
     provider_network = DynamicModelChoiceField(
         queryset=ProviderNetwork.objects.all(),
         queryset=ProviderNetwork.objects.all(),
-        query_params={
-            'provider_id': '$provider_network_provider',
-        },
-        required=False
+        required=False,
+        selector=True
     )
     )
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
         fields = [
         fields = [
-            'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network_provider',
-            'provider_network', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
-            'description', 'tags',
+            'provider', 'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed',
+            'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'port_speed': SelectSpeedWidget(),
             'port_speed': SelectSpeedWidget(),

+ 42 - 296
netbox/dcim/forms/model_forms.py

@@ -14,9 +14,9 @@ from tenancy.forms import TenancyForm
 from utilities.forms import (
 from utilities.forms import (
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
     DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK,
     DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK,
-    SlugField, SelectSpeedWidget,
+    SlugField, SelectSpeedWidget
 )
 )
-from virtualization.models import Cluster, ClusterGroup
+from virtualization.models import Cluster
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.models import WirelessLAN, WirelessLANGroup
 from .common import InterfaceCommonForm, ModuleCommonForm
 from .common import InterfaceCommonForm, ModuleCommonForm
 
 
@@ -157,26 +157,9 @@ class SiteForm(TenancyForm, NetBoxModelForm):
 
 
 
 
 class LocationForm(TenancyForm, NetBoxModelForm):
 class LocationForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     )
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
@@ -188,17 +171,14 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Location', (
-            'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tags',
-        )),
+        ('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = Location
         model = Location
         fields = (
         fields = (
-            'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
-            'tags',
+            'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags',
         )
         )
 
 
 
 
@@ -219,26 +199,9 @@ class RackRoleForm(NetBoxModelForm):
 
 
 
 
 class RackForm(TenancyForm, NetBoxModelForm):
 class RackForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     )
     location = DynamicModelChoiceField(
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
@@ -256,48 +219,16 @@ class RackForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Rack
         model = Rack
         fields = [
         fields = [
-            'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
-            'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
-            'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
+            'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
+            'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
+            'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
         ]
         ]
 
 
 
 
 class RackReservationForm(TenancyForm, NetBoxModelForm):
 class RackReservationForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        }
-    )
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-        }
+        selector=True
     )
     )
     units = NumericArrayField(
     units = NumericArrayField(
         base_field=forms.IntegerField(),
         base_field=forms.IntegerField(),
@@ -311,15 +242,14 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
+        ('Reservation', ('rack', 'units', 'user', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = RackReservation
         model = RackReservation
         fields = [
         fields = [
-            'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
-            'description', 'comments', 'tags',
+            'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
         ]
         ]
 
 
 
 
@@ -441,26 +371,9 @@ class PlatformForm(NetBoxModelForm):
 
 
 
 
 class DeviceForm(TenancyForm, NetBoxModelForm):
 class DeviceForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     )
     location = DynamicModelChoiceField(
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
@@ -491,43 +404,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             }
             }
         )
         )
     )
     )
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False,
-        initial_params={
-            'device_types': '$device_type'
-        }
-    )
     device_type = DynamicModelChoiceField(
     device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
+        selector=True
     )
     )
     device_role = DynamicModelChoiceField(
     device_role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all()
         queryset=DeviceRole.objects.all()
     )
     )
     platform = DynamicModelChoiceField(
     platform = DynamicModelChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
-        required=False,
-        query_params={
-            'manufacturer_id': ['$manufacturer', 'null']
-        }
-    )
-    cluster_group = DynamicModelChoiceField(
-        queryset=ClusterGroup.objects.all(),
-        required=False,
-        null_option='None',
-        initial_params={
-            'clusters': '$cluster'
-        }
+        required=False
     )
     )
     cluster = DynamicModelChoiceField(
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'group_id': '$cluster_group'
-        }
+        selector=True
     )
     )
     comments = CommentField()
     comments = CommentField()
     local_context_data = JSONField(
     local_context_data = JSONField(
@@ -536,7 +427,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     )
     )
     virtual_chassis = DynamicModelChoiceField(
     virtual_chassis = DynamicModelChoiceField(
         queryset=VirtualChassis.objects.all(),
         queryset=VirtualChassis.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     )
     vc_position = forms.IntegerField(
     vc_position = forms.IntegerField(
         required=False,
         required=False,
@@ -556,10 +448,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Device
         model = Device
         fields = [
         fields = [
-            'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
-            'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
-            'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
-            'description', 'config_template', 'comments', 'tags', 'local_context_data'
+            'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
+            'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant',
+            'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags',
+            'local_context_data'
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -632,18 +524,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
             'device_id': '$device'
             'device_id': '$device'
         }
         }
     )
     )
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False,
-        initial_params={
-            'module_types': '$module_type'
-        }
-    )
     module_type = DynamicModelChoiceField(
     module_type = DynamicModelChoiceField(
         queryset=ModuleType.objects.all(),
         queryset=ModuleType.objects.all(),
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
+        selector=True
     )
     )
     comments = CommentField()
     comments = CommentField()
     replicate_components = forms.BooleanField(
     replicate_components = forms.BooleanField(
@@ -651,7 +534,6 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
         initial=True,
         initial=True,
         help_text=_("Automatically populate components associated with this module type")
         help_text=_("Automatically populate components associated with this module type")
     )
     )
-
     adopt_components = forms.BooleanField(
     adopt_components = forms.BooleanField(
         required=False,
         required=False,
         initial=False,
         initial=False,
@@ -659,9 +541,7 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        ('Module', (
-            'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'description', 'tags',
-        )),
+        ('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
         ('Hardware', (
         ('Hardware', (
             'serial', 'asset_tag', 'replicate_components', 'adopt_components',
             'serial', 'asset_tag', 'replicate_components', 'adopt_components',
         )),
         )),
@@ -670,8 +550,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Module
         model = Module
         fields = [
         fields = [
-            'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag', 'tags',
-            'replicate_components', 'adopt_components', 'description', 'comments',
+            'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
+            'adopt_components', 'description', 'comments',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -702,26 +582,9 @@ class CableForm(TenancyForm, NetBoxModelForm):
 
 
 
 
 class PowerPanelForm(NetBoxModelForm):
 class PowerPanelForm(NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     )
     location = DynamicModelChoiceField(
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
@@ -733,80 +596,38 @@ class PowerPanelForm(NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'description', 'tags')),
+        ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
         fields = [
         fields = [
-            'region', 'site_group', 'site', 'location', 'name', 'description', 'comments', 'tags',
+            'site', 'location', 'name', 'description', 'comments', 'tags',
         ]
         ]
 
 
 
 
 class PowerFeedForm(NetBoxModelForm):
 class PowerFeedForm(NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites__powerpanel': '$power_panel'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        initial_params={
-            'powerpanel': '$power_panel'
-        },
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
     power_panel = DynamicModelChoiceField(
     power_panel = DynamicModelChoiceField(
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
-        query_params={
-            'site_id': '$site'
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        },
-        initial_params={
-            'racks': '$rack'
-        }
+        selector=True
     )
     )
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'location_id': '$location',
-            'site_id': '$site'
-        }
+        selector=True
     )
     )
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Power Panel', ('region', 'site', 'power_panel')),
-        ('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
+        ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
         ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
         ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerFeed
         model = PowerFeed
         fields = [
         fields = [
-            'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type',
-            'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
-            'tags',
+            'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
+            'max_utilization', 'description', 'comments', 'tags',
         ]
         ]
 
 
 
 
@@ -878,43 +699,12 @@ class DeviceVCMembershipForm(forms.ModelForm):
 
 
 
 
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        null_option='None',
-        query_params={
-            'site_id': '$site'
-        }
-    )
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         query_params={
         query_params={
-            'site_id': '$site',
-            'rack_id': '$rack',
             'virtual_chassis_id': 'null',
             'virtual_chassis_id': 'null',
-        }
+        },
+        selector=True
     )
     )
 
 
     def clean_device(self):
     def clean_device(self):
@@ -1150,7 +940,8 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
 
 
 class DeviceComponentForm(NetBoxModelForm):
 class DeviceComponentForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
+        queryset=Device.objects.all(),
+        selector=True
     )
     )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -1592,53 +1383,9 @@ class InventoryItemRoleForm(NetBoxModelForm):
 
 
 
 
 class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
 class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        },
-        initial_params={
-            'racks': '$rack'
-        }
-    )
-    rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-        }
-    )
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-            'rack_id': '$rack',
-        }
+        selector=True
     )
     )
     primary_ip4 = DynamicModelChoiceField(
     primary_ip4 = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
@@ -1660,14 +1407,13 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     )
     )
 
 
     fieldsets = (
     fieldsets = (
-        ('Assigned Device', ('region', 'site_group', 'site', 'location', 'rack', 'device')),
-        ('Virtual Device Context', ('name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
+        ('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant'))
         ('Tenancy', ('tenant_group', 'tenant'))
     )
     )
 
 
     class Meta:
     class Meta:
         model = VirtualDeviceContext
         model = VirtualDeviceContext
         fields = [
         fields = [
-            'region', 'site_group', 'site', 'location', 'rack', 'device', 'name', 'status', 'identifier',
-            'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags'
+            'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
+            'comments', 'tags'
         ]
         ]

+ 19 - 154
netbox/ipam/forms/model_forms.py

@@ -200,40 +200,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         label=_('VRF')
         label=_('VRF')
     )
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    vlan_group = DynamicModelChoiceField(
-        queryset=VLANGroup.objects.all(),
-        required=False,
-        label=_('VLAN group'),
-        null_option='None',
-        query_params={
-            'site': '$site'
-        },
-        initial_params={
-            'vlans': '$vlan'
-        }
+        selector=True,
+        null_option='None'
     )
     )
     vlan = DynamicModelChoiceField(
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
@@ -241,7 +212,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         label=_('VLAN'),
         label=_('VLAN'),
         query_params={
         query_params={
             'site_id': '$site',
             'site_id': '$site',
-            'group_id': '$vlan_group',
         }
         }
     )
     )
     role = DynamicModelChoiceField(
     role = DynamicModelChoiceField(
@@ -252,7 +222,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
         ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
         ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
-        ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
+        ('Site/VLAN Assignment', ('site', 'vlan')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
     )
 
 
@@ -329,65 +299,22 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         label=_('VRF')
         label=_('VRF')
     )
     )
-    nat_region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        label=_('Region'),
-        initial_params={
-            'sites': '$nat_site'
-        }
-    )
-    nat_site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        label=_('Site group'),
-        initial_params={
-            'sites': '$nat_site'
-        }
-    )
-    nat_site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        label=_('Site'),
-        query_params={
-            'region_id': '$nat_region',
-            'group_id': '$nat_site_group',
-        }
-    )
-    nat_rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        label=_('Rack'),
-        null_option='None',
-        query_params={
-            'site_id': '$site'
-        }
-    )
     nat_device = DynamicModelChoiceField(
     nat_device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         required=False,
         required=False,
-        label=_('Device'),
-        query_params={
-            'site_id': '$site',
-            'rack_id': '$nat_rack',
-        }
-    )
-    nat_cluster = DynamicModelChoiceField(
-        queryset=Cluster.objects.all(),
-        required=False,
-        label=_('Cluster')
+        selector=True,
+        label=_('Device')
     )
     )
     nat_virtual_machine = DynamicModelChoiceField(
     nat_virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),
         required=False,
         required=False,
-        label=_('Virtual Machine'),
-        query_params={
-            'cluster_id': '$nat_cluster',
-        }
+        selector=True,
+        label=_('Virtual Machine')
     )
     )
     nat_vrf = DynamicModelChoiceField(
     nat_vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
+        selector=True,
         label=_('VRF')
         label=_('VRF')
     )
     )
     nat_inside = DynamicModelChoiceField(
     nat_inside = DynamicModelChoiceField(
@@ -409,9 +336,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
         fields = [
         fields = [
-            'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_site', 'nat_rack', 'nat_device',
-            'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description',
-            'comments', 'tags',
+            'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_device', 'nat_virtual_machine',
+            'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -714,58 +640,18 @@ class VLANGroupForm(NetBoxModelForm):
 
 
 
 
 class VLANForm(TenancyForm, NetBoxModelForm):
 class VLANForm(TenancyForm, NetBoxModelForm):
-    # VLANGroup assignment fields
-    scope_type = forms.ChoiceField(
-        choices=(
-            ('', ''),
-            ('dcim.region', 'Region'),
-            ('dcim.sitegroup', 'Site group'),
-            ('dcim.site', 'Site'),
-            ('dcim.location', 'Location'),
-            ('dcim.rack', 'Rack'),
-            ('virtualization.clustergroup', 'Cluster group'),
-            ('virtualization.cluster', 'Cluster'),
-        ),
-        required=False,
-        label=_('Group scope')
-    )
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'scope_type': '$scope_type',
-        },
+        selector=True,
         label=_('VLAN Group')
         label=_('VLAN Group')
     )
     )
-
-    # Site assignment fields
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        },
-        label=_('Region')
-    )
-    sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        },
-        label=_('Site group')
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         null_option='None',
         null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$sitegroup',
-        }
+        selector=True
     )
     )
-
-    # Other fields
     role = DynamicModelChoiceField(
     role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         required=False
         required=False
@@ -804,11 +690,13 @@ class ServiceTemplateForm(NetBoxModelForm):
 class ServiceForm(NetBoxModelForm):
 class ServiceForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     )
     virtual_machine = DynamicModelChoiceField(
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     )
     ports = NumericArrayField(
     ports = NumericArrayField(
         base_field=forms.IntegerField(
         base_field=forms.IntegerField(
@@ -908,43 +796,21 @@ class L2VPNTerminationForm(NetBoxModelForm):
         label=_('L2VPN'),
         label=_('L2VPN'),
         fetch_trigger='open'
         fetch_trigger='open'
     )
     )
-    device_vlan = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        label=_("Available on Device"),
-        required=False,
-        query_params={}
-    )
     vlan = DynamicModelChoiceField(
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'available_on_device': '$device_vlan'
-        },
+        selector=True,
         label=_('VLAN')
         label=_('VLAN')
     )
     )
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        required=False,
-        query_params={}
-    )
     interface = DynamicModelChoiceField(
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'device_id': '$device'
-        }
-    )
-    virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all(),
-        required=False,
-        query_params={}
+        selector=True
     )
     )
     vminterface = DynamicModelChoiceField(
     vminterface = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         queryset=VMInterface.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'virtual_machine_id': '$virtual_machine'
-        },
+        selector=True,
         label=_('Interface')
         label=_('Interface')
     )
     )
 
 
@@ -958,7 +824,6 @@ class L2VPNTerminationForm(NetBoxModelForm):
 
 
         if instance:
         if instance:
             if type(instance.assigned_object) is Interface:
             if type(instance.assigned_object) is Interface:
-                initial['device'] = instance.assigned_object.parent
                 initial['interface'] = instance.assigned_object
                 initial['interface'] = instance.assigned_object
             elif type(instance.assigned_object) is VLAN:
             elif type(instance.assigned_object) is VLAN:
                 initial['vlan'] = instance.assigned_object
                 initial['vlan'] = instance.assigned_object

+ 4 - 1
netbox/netbox/urls.py

@@ -10,7 +10,7 @@ from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_a
 from netbox.api.views import APIRootView, StatusView
 from netbox.api.views import APIRootView, StatusView
 from netbox.graphql.schema import schema
 from netbox.graphql.schema import schema
 from netbox.graphql.views import GraphQLView
 from netbox.graphql.views import GraphQLView
-from netbox.views import HomeView, StaticMediaFailureView, SearchView
+from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
 from users.views import LoginView, LogoutView
 from users.views import LoginView, LogoutView
 from .admin import admin_site
 from .admin import admin_site
 
 
@@ -51,6 +51,9 @@ _patterns = [
     path('virtualization/', include('virtualization.urls')),
     path('virtualization/', include('virtualization.urls')),
     path('wireless/', include('wireless.urls')),
     path('wireless/', include('wireless.urls')),
 
 
+    # HTMX views
+    path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),
+
     # API
     # API
     path('api/', APIRootView.as_view(), name='api-root'),
     path('api/', APIRootView.as_view(), name='api-root'),
     path('api/circuits/', include('circuits.api.urls')),
     path('api/circuits/', include('circuits.api.urls')),

+ 56 - 0
netbox/netbox/views/htmx.py

@@ -0,0 +1,56 @@
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import Http404
+from django.shortcuts import render
+from django.utils.module_loading import import_string
+from django.views.generic import View
+
+
+class ObjectSelectorView(View):
+    template_name = 'htmx/object_selector.html'
+
+    def get(self, request):
+        model = self._get_model(request.GET.get('_model', ''))
+
+        form_class = self._get_form_class(model)
+        form = form_class(request.GET)
+
+        if '_search' in request.GET:
+            # Return only search results
+            filterset = self._get_filterset_class(model)
+
+            queryset = model.objects.restrict(request.user)
+            if filterset:
+                queryset = filterset(request.GET, queryset, request=request).qs
+
+            return render(request, 'htmx/object_selector_results.html', {
+                'results': queryset[:100],
+            })
+
+        return render(request, self.template_name, {
+            'form': form,
+            'model': model,
+            'target_id': request.GET.get('target'),
+        })
+
+    def _get_model(self, label):
+        try:
+            app_label, model_name = label.split('.')
+            content_type = ContentType.objects.get_by_natural_key(app_label, model_name)
+        except (ValueError, ObjectDoesNotExist):
+            raise Http404
+        return content_type.model_class()
+
+    def _get_form_class(self, model):
+        if hasattr(self, 'form_class'):
+            return self.form_class
+        app_label = model._meta.app_label
+        class_name = f'{model.__name__}FilterForm'
+        return import_string(f'{app_label}.forms.{class_name}')
+
+    def _get_filterset_class(self, model):
+        if hasattr(self, 'filterset_class'):
+            return self.filterset_class
+        app_label = model._meta.app_label
+        class_name = f'{model.__name__}FilterSet'
+        return import_string(f'{app_label}.filtersets.{class_name}')

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 2 - 1
netbox/project-static/src/htmx.ts

@@ -1,9 +1,10 @@
 import { getElements, isTruthy } from './util';
 import { getElements, isTruthy } from './util';
 import { initButtons } from './buttons';
 import { initButtons } from './buttons';
 import { initSelect } from './select';
 import { initSelect } from './select';
+import { initObjectSelector } from './objectSelector';
 
 
 function initDepedencies(): void {
 function initDepedencies(): void {
-  for (const init of [initButtons, initSelect]) {
+  for (const init of [initButtons, initSelect, initObjectSelector]) {
     init();
     init();
   }
   }
 }
 }

+ 32 - 0
netbox/project-static/src/objectSelector.ts

@@ -0,0 +1,32 @@
+import { getElements } from './util';
+
+function handleSelection(link: HTMLAnchorElement): void {
+  const selector_results = document.getElementById('selector_results');
+  if (selector_results == null) {
+    return
+  }
+  const target_id = selector_results.getAttribute('data-selector-target');
+  if (target_id == null) {
+    return
+  }
+  const target = document.getElementById(target_id);
+  if (target == null) {
+    return
+  }
+
+  const label = link.getAttribute('data-label');
+  const value = link.getAttribute('data-value');
+
+  //@ts-ignore
+  target.slim.setData([
+    {text: label, value: value}
+  ]);
+
+}
+
+
+export function initObjectSelector(): void {
+  for (const element of getElements<HTMLAnchorElement>('#selector_results a')) {
+    element.addEventListener('click', () => handleSelection(element));
+  }
+}

+ 0 - 3
netbox/templates/circuits/circuittermination_edit.html

@@ -27,12 +27,9 @@
       </div>
       </div>
       <div class="tab-content p-0 border-0">
       <div class="tab-content p-0 border-0">
         <div class="tab-pane{% if not providernetwork_tab_active %} active{% endif %}" id="site">
         <div class="tab-pane{% if not providernetwork_tab_active %} active{% endif %}" id="site">
-          {% render_field form.region %}
-          {% render_field form.site_group %}
           {% render_field form.site %}
           {% render_field form.site %}
         </div>
         </div>
         <div class="tab-pane{% if providernetwork_tab_active %} active{% endif %}" id="providernetwork">
         <div class="tab-pane{% if providernetwork_tab_active %} active{% endif %}" id="providernetwork">
-          {% render_field form.provider_network_provider %}
           {% render_field form.provider_network %}
           {% render_field form.provider_network %}
         </div>
         </div>
       </div>
       </div>

+ 0 - 4
netbox/templates/dcim/device_edit.html

@@ -18,7 +18,6 @@
       <div class="row mb-2">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Hardware</h5>
         <h5 class="offset-sm-3">Hardware</h5>
       </div>
       </div>
-      {% render_field form.manufacturer %}
       {% render_field form.device_type %}
       {% render_field form.device_type %}
       {% render_field form.airflow %}
       {% render_field form.airflow %}
       {% render_field form.serial %}
       {% render_field form.serial %}
@@ -29,8 +28,6 @@
       <div class="row mb-2">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Location</h5>
         <h5 class="offset-sm-3">Location</h5>
       </div>
       </div>
-      {% render_field form.region %}
-      {% render_field form.site_group %}
       {% render_field form.site %}
       {% render_field form.site %}
       {% render_field form.location %}
       {% render_field form.location %}
       {% render_field form.rack %}
       {% render_field form.rack %}
@@ -76,7 +73,6 @@
       <div class="row mb-2">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Virtualization</h5>
         <h5 class="offset-sm-3">Virtualization</h5>
       </div>
       </div>
-      {% render_field form.cluster_group %}
       {% render_field form.cluster %}
       {% render_field form.cluster %}
     </div>
     </div>
     
     

+ 0 - 2
netbox/templates/dcim/rack_edit.html

@@ -6,8 +6,6 @@
         <div class="row mb-2">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Rack</h5>
           <h5 class="offset-sm-3">Rack</h5>
         </div>
         </div>
-        {% render_field form.region %}
-        {% render_field form.site_group %}
         {% render_field form.site %}
         {% render_field form.site %}
         {% render_field form.location %}
         {% render_field form.location %}
         {% render_field form.name %}
         {% render_field form.name %}

+ 4 - 0
netbox/templates/generic/object_edit.html

@@ -74,3 +74,7 @@ Context:
   </div>
   </div>
 
 
 {% endblock content-wrapper %}
 {% endblock content-wrapper %}
+
+{% block modals %}
+  {% include 'inc/htmx_modal.html' with size='lg' %}
+{% endblock %}

+ 32 - 0
netbox/templates/htmx/object_selector.html

@@ -0,0 +1,32 @@
+{% load form_helpers %}
+
+<div class="modal-header">
+  <h5 class="modal-title">Select {{ model|meta:"verbose_name"|bettertitle }}</h5>
+  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+</div>
+<div class="modal-body row">
+  <div class="col-3">
+    <div class="list-group list-group-flush">
+      {% for field in form.visible_fields %}
+        <a href="#" class="list-group-item list-group-item-action px-0 py-1" data-bs-toggle="collapse" data-bs-target="#checkmark{{ forloop.counter }}, #selector{{ forloop.counter }}">
+          <span id="checkmark{{ forloop.counter }}" class="collapse{% if forloop.counter < 3 %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
+          {{ field.label }}
+        </a>
+      {% endfor %}
+    </div>
+  </div>
+  <div class="col-9">
+    <form hx-get="{% url 'htmx_object_selector' %}?_model={{ model|meta:"label_lower" }}" hx-target="#selector_results" hx-trigger="load, submit, keyup from:#id_q delay:500ms">
+      <input type="hidden" name="_search" value="true" />
+      <div class="tab-content p-1">
+        {% for field in form.visible_fields %}
+          <div class="collapse{% if forloop.counter < 3 %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
+        {% endfor %}
+      </div>
+      <div class="text-end">
+        <button type="submit" class="btn btn-sm btn-primary">Search</button>
+      </div>
+    </form>
+    <div id="selector_results" class="mt-3" data-selector-target="{{ target_id }}"></div>
+  </div>
+</div>

+ 13 - 0
netbox/templates/htmx/object_selector_results.html

@@ -0,0 +1,13 @@
+<div class="list-group">
+  {% for object in results %}
+    <a href="#" class="list-group-item list-group-item-action" data-label="{{ object }}" data-value="{{ object.pk }}" data-bs-dismiss="modal">
+      <h6 class="mb-1">
+        {{ object }}
+        {% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}
+      </h6>
+      {% if object.description %}
+        <small>{{ object.description }}</small>
+      {% endif %}
+    </a>
+  {% endfor %}
+</div>

+ 1 - 1
netbox/templates/inc/htmx_modal.html

@@ -1,5 +1,5 @@
 <div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
 <div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
-  <div class="modal-dialog">
+  <div class="modal-dialog{% if size %} modal-{{ size }}{% endif %}">
     <div class="modal-content" id="htmx-modal-content">
     <div class="modal-content" id="htmx-modal-content">
       {# Dynamic content goes here #}
       {# Dynamic content goes here #}
     </div>
     </div>

+ 0 - 5
netbox/templates/ipam/ipaddress_edit.html

@@ -121,14 +121,9 @@
       </div>
       </div>
       <div class="tab-content p-0 border-0">
       <div class="tab-content p-0 border-0">
           <div class="tab-pane active" id="by_device" aria-labelledby="device_tab" role="tabpanel">
           <div class="tab-pane active" id="by_device" aria-labelledby="device_tab" role="tabpanel">
-              {% render_field form.nat_region %}
-              {% render_field form.nat_site_group %}
-              {% render_field form.nat_site %}
-              {% render_field form.nat_rack %}
               {% render_field form.nat_device %}
               {% render_field form.nat_device %}
           </div>
           </div>
           <div class="tab-pane" id="by_vm" aria-labelledby="vm_tab" role="tabpanel">
           <div class="tab-pane" id="by_vm" aria-labelledby="vm_tab" role="tabpanel">
-              {% render_field form.nat_cluster %}
               {% render_field form.nat_virtual_machine %}
               {% render_field form.nat_virtual_machine %}
           </div>
           </div>
           <div class="tab-pane" id="by_vrf" aria-labelledby="vrf_tab" role="tabpanel">
           <div class="tab-pane" id="by_vrf" aria-labelledby="vrf_tab" role="tabpanel">

+ 0 - 3
netbox/templates/ipam/l2vpntermination_edit.html

@@ -32,15 +32,12 @@
     <div class="row mb-3">
     <div class="row mb-3">
       <div class="tab-content p-0 border-0">
       <div class="tab-content p-0 border-0">
         <div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
         <div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
-          {% render_field form.device_vlan %}
           {% render_field form.vlan %}
           {% render_field form.vlan %}
         </div>
         </div>
         <div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
         <div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
-          {% render_field form.device %}
           {% render_field form.interface %}
           {% render_field form.interface %}
         </div>
         </div>
         <div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vminterface" role="tabpanel" aria-labeled-by="vminterface_tab">
         <div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vminterface" role="tabpanel" aria-labeled-by="vminterface_tab">
-          {% render_field form.virtual_machine %}
           {% render_field form.vminterface %}
           {% render_field form.vminterface %}
         </div>
         </div>
       </div>
       </div>

+ 0 - 3
netbox/templates/ipam/vlan_edit.html

@@ -43,12 +43,9 @@
       </div>
       </div>
       <div class="tab-content p-0 border-0">
       <div class="tab-content p-0 border-0">
         <div class="tab-pane{% if not site_tab_active %} active{% endif %}" id="group">
         <div class="tab-pane{% if not site_tab_active %} active{% endif %}" id="group">
-          {% render_field form.scope_type %}
           {% render_field form.group %}
           {% render_field form.group %}
         </div>
         </div>
         <div class="tab-pane{% if site_tab_active %} active{% endif %}" id="site">
         <div class="tab-pane{% if site_tab_active %} active{% endif %}" id="site">
-          {% render_field form.region %}
-          {% render_field form.sitegroup %}
           {% render_field form.site %}
           {% render_field form.site %}
         </div>
         </div>
       </div>
       </div>

+ 21 - 3
netbox/utilities/forms/fields/dynamic.py

@@ -26,24 +26,38 @@ class DynamicModelChoiceMixin:
             choice (optional)
             choice (optional)
         fetch_trigger: The event type which will cause the select element to
         fetch_trigger: The event type which will cause the select element to
             fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
             fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
+        selector: Include an advanced object selection widget to assist the user in identifying the desired object
     """
     """
     filter = django_filters.ModelChoiceFilter
     filter = django_filters.ModelChoiceFilter
     widget = widgets.APISelect
     widget = widgets.APISelect
 
 
-    def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None,
-                 fetch_trigger=None, empty_label=None, *args, **kwargs):
+    def __init__(
+            self,
+            queryset,
+            *,
+            query_params=None,
+            initial_params=None,
+            null_option=None,
+            disabled_indicator=None,
+            fetch_trigger=None,
+            empty_label=None,
+            selector=False,
+            **kwargs
+    ):
+        self.model = queryset.model
         self.query_params = query_params or {}
         self.query_params = query_params or {}
         self.initial_params = initial_params or {}
         self.initial_params = initial_params or {}
         self.null_option = null_option
         self.null_option = null_option
         self.disabled_indicator = disabled_indicator
         self.disabled_indicator = disabled_indicator
         self.fetch_trigger = fetch_trigger
         self.fetch_trigger = fetch_trigger
+        self.selector = selector
 
 
         # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
         # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
         # by widget_attrs()
         # by widget_attrs()
         self.to_field_name = kwargs.get('to_field_name')
         self.to_field_name = kwargs.get('to_field_name')
         self.empty_option = empty_label or ""
         self.empty_option = empty_label or ""
 
 
-        super().__init__(*args, **kwargs)
+        super().__init__(queryset, **kwargs)
 
 
     def widget_attrs(self, widget):
     def widget_attrs(self, widget):
         attrs = {
         attrs = {
@@ -70,6 +84,10 @@ class DynamicModelChoiceMixin:
         if (len(self.query_params) > 0):
         if (len(self.query_params) > 0):
             widget.add_query_params(self.query_params)
             widget.add_query_params(self.query_params)
 
 
+        # Include object selector?
+        if self.selector:
+            attrs['selector'] = self.model._meta.label_lower
+
         return attrs
         return attrs
 
 
     def get_bound_field(self, form, field_name):
     def get_bound_field(self, form, field_name):

+ 1 - 0
netbox/utilities/forms/widgets.py

@@ -121,6 +121,7 @@ class APISelect(forms.Select):
 
 
     :param api_url: API endpoint URL. Required if not set automatically by the parent field.
     :param api_url: API endpoint URL. Required if not set automatically by the parent field.
     """
     """
+    template_name = 'widgets/apiselect.html'
     option_template_name = 'widgets/select_option.html'
     option_template_name = 'widgets/select_option.html'
     dynamic_params: Dict[str, str]
     dynamic_params: Dict[str, str]
     static_params: Dict[str, List[str]]
     static_params: Dict[str, List[str]]

+ 18 - 0
netbox/utilities/templates/widgets/apiselect.html

@@ -0,0 +1,18 @@
+{% if widget.attrs.selector %}
+  <div class="d-flex">
+    {% include 'django/forms/widgets/select.html' %}
+    <button
+      type="button"
+      title="Open selector"
+      class="btn btn-sm btn-outline-dark border-input ms-1"
+      data-bs-toggle="modal"
+      data-bs-target="#htmx-modal"
+      hx-get="{% url 'htmx_object_selector' %}?_model={{ widget.attrs.selector }}&target={{ widget.attrs.id }}"
+      hx-target="#htmx-modal-content"
+    >
+      <i class="mdi mdi-database-search-outline"></i>
+    </button>
+  </div>
+{% else %}
+  {% include 'django/forms/widgets/select.html' %}
+{% endif %}

+ 9 - 36
netbox/virtualization/forms/model_forms.py

@@ -65,41 +65,22 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False
         required=False
     )
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     )
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Cluster', ('name', 'type', 'group', 'status', 'description', 'tags')),
-        ('Site', ('region', 'site_group', 'site')),
+        ('Cluster', ('name', 'type', 'group', 'site', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster
         fields = (
         fields = (
-            'name', 'type', 'group', 'status', 'tenant', 'region', 'site_group', 'site', 'description', 'comments',
-            'tags',
+            'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', 'tags',
         )
         )
 
 
 
 
@@ -178,20 +159,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False
         required=False
     )
     )
-    cluster_group = DynamicModelChoiceField(
-        queryset=ClusterGroup.objects.all(),
-        required=False,
-        null_option='None',
-        initial_params={
-            'clusters': '$cluster'
-        }
-    )
     cluster = DynamicModelChoiceField(
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
         required=False,
         required=False,
+        selector=True,
         query_params={
         query_params={
             'site_id': '$site',
             'site_id': '$site',
-            'group_id': '$cluster_group',
         }
         }
     )
     )
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
@@ -222,7 +195,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
         ('Virtual Machine', ('name', 'role', 'status', 'description', 'tags')),
         ('Virtual Machine', ('name', 'role', 'status', 'description', 'tags')),
-        ('Site/Cluster', ('site', 'cluster_group', 'cluster', 'device')),
+        ('Site/Cluster', ('site', 'cluster', 'device')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
         ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
         ('Resources', ('vcpus', 'memory', 'disk')),
         ('Resources', ('vcpus', 'memory', 'disk')),
@@ -232,9 +205,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = VirtualMachine
         model = VirtualMachine
         fields = [
         fields = [
-            'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
-            'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags',
-            'local_context_data',
+            'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
+            'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -280,7 +252,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     virtual_machine = DynamicModelChoiceField(
     virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all()
+        queryset=VirtualMachine.objects.all(),
+        selector=True
     )
     )
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         queryset=VMInterface.objects.all(),

+ 5 - 44
netbox/wireless/forms/model_forms.py

@@ -38,55 +38,16 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
         queryset=WirelessLANGroup.objects.all(),
         queryset=WirelessLANGroup.objects.all(),
         required=False
         required=False
     )
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    vlan_group = DynamicModelChoiceField(
-        queryset=VLANGroup.objects.all(),
-        required=False,
-        label=_('VLAN group'),
-        null_option='None',
-        query_params={
-            'site': '$site'
-        },
-        initial_params={
-            'vlans': '$vlan'
-        }
-    )
     vlan = DynamicModelChoiceField(
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
-        label=_('VLAN'),
-        query_params={
-            'site_id': '$site',
-            'group_id': '$vlan_group',
-        }
+        selector=True,
+        label=_('VLAN')
     )
     )
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Wireless LAN', ('ssid', 'group', 'status', 'description', 'tags')),
-        ('VLAN', ('region', 'site_group', 'site', 'vlan_group', 'vlan',)),
+        ('Wireless LAN', ('ssid', 'group', 'vlan', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
         ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
     )
     )
@@ -94,8 +55,8 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = WirelessLAN
         model = WirelessLAN
         fields = [
         fields = [
-            'ssid', 'group', 'region', 'site_group', 'site', 'status', 'vlan_group', 'vlan', 'tenant_group', 'tenant',
-            'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags',
+            'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
+            'description', 'comments', 'tags',
         ]
         ]
 
 
 
 

Некоторые файлы не были показаны из-за большого количества измененных файлов