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

Closes #17256: Fix translation support in VLAN group scope assignment form (#17270)

* Closes #17256: Fix translation support in VLAN group scope assignment form

* Disable scope field if scope type not selected; update label on type change

* Reset selected scope object when changing scope type
Jeremy Stretch 1 год назад
Родитель
Сommit
c2d67fa17e

+ 33 - 82
netbox/ipam/forms/model_forms.py

@@ -1,9 +1,9 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.utils.translation import gettext_lazy as _
 
-from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
+from dcim.models import Device, Interface, Site
 from ipam.choices import *
 from ipam.constants import *
 from ipam.formfields import IPNetworkFormField
@@ -17,8 +17,10 @@ from utilities.forms.fields import (
     SlugField,
 )
 from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
-from utilities.forms.widgets import DatePicker
-from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
+from utilities.forms.utils import get_field_value
+from utilities.forms.widgets import DatePicker, HTMXSelect
+from utilities.templatetags.builtins.filters import bettertitle
+from virtualization.models import VirtualMachine, VMInterface
 
 __all__ = (
     'AggregateForm',
@@ -562,91 +564,31 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
 
 
 class VLANGroupForm(NetBoxModelForm):
+    slug = SlugField()
     scope_type = ContentTypeChoiceField(
-        label=_('Scope type'),
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
-        required=False
-    )
-    region = DynamicModelChoiceField(
-        label=_('Region'),
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
+        widget=HTMXSelect(),
         required=False,
-        initial_params={
-            'sites': '$site'
-        },
-        label=_('Site group')
+        label=_('Scope type')
     )
-    site = DynamicModelChoiceField(
-        label=_('Site'),
-        queryset=Site.objects.all(),
+    scope = DynamicModelChoiceField(
+        label=_('Scope'),
+        queryset=Site.objects.none(),  # Initial queryset
         required=False,
-        initial_params={
-            'locations': '$location'
-        },
-        query_params={
-            'region_id': '$region',
-            'group_id': '$sitegroup',
-        }
-    )
-    location = DynamicModelChoiceField(
-        label=_('Location'),
-        queryset=Location.objects.all(),
-        required=False,
-        initial_params={
-            'racks': '$rack'
-        },
-        query_params={
-            'site_id': '$site',
-        }
-    )
-    rack = DynamicModelChoiceField(
-        label=_('Rack'),
-        queryset=Rack.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-        }
-    )
-    clustergroup = DynamicModelChoiceField(
-        queryset=ClusterGroup.objects.all(),
-        required=False,
-        initial_params={
-            'clusters': '$cluster'
-        },
-        label=_('Cluster group')
-    )
-    cluster = DynamicModelChoiceField(
-        label=_('Cluster'),
-        queryset=Cluster.objects.all(),
-        required=False,
-        query_params={
-            'group_id': '$clustergroup',
-        }
+        disabled=True,
+        selector=True
     )
-    slug = SlugField()
 
     fieldsets = (
         FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
         FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
-        FieldSet(
-            'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
-            name=_('Scope')
-        ),
+        FieldSet('scope_type', 'scope', name=_('Scope')),
     )
 
     class Meta:
         model = VLANGroup
         fields = [
-            'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
-            'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
+            'name', 'slug', 'description', 'min_vid', 'max_vid', 'scope_type', 'scope', 'tags',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -654,21 +596,30 @@ class VLANGroupForm(NetBoxModelForm):
         initial = kwargs.get('initial', {})
 
         if instance is not None and instance.scope:
-            initial[instance.scope_type.model] = instance.scope
-
+            initial['scope'] = instance.scope
             kwargs['initial'] = initial
 
         super().__init__(*args, **kwargs)
 
+        if scope_type_id := get_field_value(self, 'scope_type'):
+            try:
+                scope_type = ContentType.objects.get(pk=scope_type_id)
+                model = scope_type.model_class()
+                self.fields['scope'].queryset = model.objects.all()
+                self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
+                self.fields['scope'].disabled = False
+                self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
+            except ObjectDoesNotExist:
+                pass
+
+            if self.instance and scope_type_id != self.instance.scope_type_id:
+                self.initial['scope'] = None
+
     def clean(self):
         super().clean()
 
-        # Assign scope based on scope_type
-        if self.cleaned_data.get('scope_type'):
-            scope_field = self.cleaned_data['scope_type'].model
-            self.instance.scope = self.cleaned_data.get(scope_field)
-        else:
-            self.instance.scope_id = None
+        # Assign the selected scope (if any)
+        self.instance.scope = self.cleaned_data.get('scope')
 
 
 class VLANForm(TenancyForm, NetBoxModelForm):

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


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


+ 1 - 2
netbox/project-static/src/forms/index.ts

@@ -1,9 +1,8 @@
 import { initFormElements } from './elements';
 import { initSpeedSelector } from './speedSelector';
-import { initScopeSelector } from './scopeSelector';
 
 export function initForms(): void {
-  for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
+  for (const func of [initFormElements, initSpeedSelector]) {
     func();
   }
 }

+ 0 - 153
netbox/project-static/src/forms/scopeSelector.ts

@@ -1,153 +0,0 @@
-import { getElements, toggleVisibility } from '../util';
-
-type ShowHideMap = {
-  /**
-   * Name of view to which this map should apply.
-   *
-   * @example vlangroup_edit
-   */
-  [view: string]: string;
-};
-
-type ShowHideLayout = {
-  /**
-   * Name of layout config
-   *
-   * @example vlangroup
-   */
-  [config: string]: {
-    /**
-     * Default layout.
-     */
-    default: { hide: string[]; show: string[] };
-    /**
-     * Field name to layout mapping.
-     */
-    [fieldName: string]: { hide: string[]; show: string[] };
-  };
-};
-
-/**
- * Mapping of layout names to arrays of object types whose fields should be hidden or shown when
- * the scope type (key) is selected.
- *
- * For example, if `region` is the scope type, the fields with IDs listed in
- * showHideMap.region.hide should be hidden, and the fields with IDs listed in
- * showHideMap.region.show should be shown.
- */
-const showHideLayout: ShowHideLayout = {
-  vlangroup: {
-    region: {
-      hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
-      show: ['id_region'],
-    },
-    'site group': {
-      hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
-      show: ['id_sitegroup'],
-    },
-    site: {
-      hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
-      show: ['id_region', 'id_sitegroup', 'id_site'],
-    },
-    location: {
-      hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
-      show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
-    },
-    rack: {
-      hide: ['id_clustergroup', 'id_cluster'],
-      show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
-    },
-    'cluster group': {
-      hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
-      show: ['id_clustergroup'],
-    },
-    cluster: {
-      hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
-      show: ['id_clustergroup', 'id_cluster'],
-    },
-    default: {
-      hide: [
-        'id_region',
-        'id_sitegroup',
-        'id_site',
-        'id_location',
-        'id_rack',
-        'id_clustergroup',
-        'id_cluster',
-      ],
-      show: [],
-    },
-  },
-};
-
-/**
- * Mapping of view names to layout configurations
- *
- * For example, if `vlangroup_add` is the view, use the layout configuration `vlangroup`.
- */
-const showHideMap: ShowHideMap = {
-  vlangroup_add: 'vlangroup',
-  vlangroup_edit: 'vlangroup',
-  vlangroup_bulk_edit: 'vlangroup',
-};
-
-/**
- * Toggle visibility of a given element's parent.
- * @param query CSS Query.
- * @param action Show or Hide the Parent.
- */
-function toggleParentVisibility(query: string, action: 'show' | 'hide') {
-  for (const element of getElements(query)) {
-    const parent = element.parentElement?.parentElement as Nullable<HTMLDivElement>;
-    if (parent !== null) {
-      if (action === 'show') {
-        toggleVisibility(parent, 'show');
-      } else {
-        toggleVisibility(parent, 'hide');
-      }
-    }
-  }
-}
-
-/**
- * Handle changes to the Scope Type field.
- */
-function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSelectElement) {
-  // Scope type's innerText looks something like `DCIM > region`.
-  const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
-  const layoutConfig = showHideMap[view];
-
-  for (const [scope, fields] of Object.entries(showHideLayout[layoutConfig])) {
-    // If the scope type ends with the specified scope, toggle its field visibility according to
-    // the show/hide values.
-    if (scopeType.endsWith(scope)) {
-      for (const field of fields.hide) {
-        toggleParentVisibility(`#${field}`, 'hide');
-      }
-      for (const field of fields.show) {
-        toggleParentVisibility(`#${field}`, 'show');
-      }
-      // Stop on first match.
-      break;
-    } else {
-      // Otherwise, hide all fields.
-      for (const field of showHideLayout[layoutConfig].default.hide) {
-        toggleParentVisibility(`#${field}`, 'hide');
-      }
-    }
-  }
-}
-
-/**
- * Initialize scope type select event listeners.
- */
-export function initScopeSelector(): void {
-  for (const view of Object.keys(showHideMap)) {
-    for (const element of getElements<HTMLSelectElement>(
-      `html[data-netbox-url-name="${view}"] #id_scope_type`,
-    )) {
-      handleScopeChange(view, element);
-      element.addEventListener('change', () => handleScopeChange(view, element));
-    }
-  }
-}

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