mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
* 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
This commit is contained in:
parent
4f225b4e56
commit
c2d67fa17e
@ -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):
|
||||
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(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
label=_('Site group')
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
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',
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
scope_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Scope type')
|
||||
)
|
||||
scope = DynamicModelChoiceField(
|
||||
label=_('Scope'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
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):
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user