mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -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 import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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 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.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.formfields import IPNetworkFormField
|
from ipam.formfields import IPNetworkFormField
|
||||||
@ -17,8 +17,10 @@ from utilities.forms.fields import (
|
|||||||
SlugField,
|
SlugField,
|
||||||
)
|
)
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
||||||
from utilities.forms.widgets import DatePicker
|
from utilities.forms.utils import get_field_value
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
from utilities.forms.widgets import DatePicker, HTMXSelect
|
||||||
|
from utilities.templatetags.builtins.filters import bettertitle
|
||||||
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateForm',
|
'AggregateForm',
|
||||||
@ -562,91 +564,31 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupForm(NetBoxModelForm):
|
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()
|
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 = (
|
fieldsets = (
|
||||||
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
|
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
|
||||||
FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
|
FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
|
||||||
FieldSet(
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||||
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
|
|
||||||
name=_('Scope')
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
'name', 'slug', 'description', 'min_vid', 'max_vid', 'scope_type', 'scope', 'tags',
|
||||||
'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -654,21 +596,30 @@ class VLANGroupForm(NetBoxModelForm):
|
|||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {})
|
||||||
|
|
||||||
if instance is not None and instance.scope:
|
if instance is not None and instance.scope:
|
||||||
initial[instance.scope_type.model] = instance.scope
|
initial['scope'] = instance.scope
|
||||||
|
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
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):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Assign scope based on scope_type
|
# Assign the selected scope (if any)
|
||||||
if self.cleaned_data.get('scope_type'):
|
self.instance.scope = self.cleaned_data.get('scope')
|
||||||
scope_field = self.cleaned_data['scope_type'].model
|
|
||||||
self.instance.scope = self.cleaned_data.get(scope_field)
|
|
||||||
else:
|
|
||||||
self.instance.scope_id = None
|
|
||||||
|
|
||||||
|
|
||||||
class VLANForm(TenancyForm, NetBoxModelForm):
|
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 { initFormElements } from './elements';
|
||||||
import { initSpeedSelector } from './speedSelector';
|
import { initSpeedSelector } from './speedSelector';
|
||||||
import { initScopeSelector } from './scopeSelector';
|
|
||||||
|
|
||||||
export function initForms(): void {
|
export function initForms(): void {
|
||||||
for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
|
for (const func of [initFormElements, initSpeedSelector]) {
|
||||||
func();
|
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