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
This commit is contained in:
Jeremy Stretch 2024-08-27 13:58:56 -04:00 committed by GitHub
parent 4f225b4e56
commit c2d67fa17e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 39 additions and 242 deletions

View File

@ -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):

Binary file not shown.

Binary file not shown.

View File

@ -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();
}
}

View File

@ -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));
}
}
}