Compare commits

...

2 Commits

Author SHA1 Message Date
Jeremy Stretch
75193ee5fc Initial work on #21259 2026-01-23 16:28:07 -05:00
Arthur Hanson
a9a300197a Clear Rack Face when clear Rack (#21182)
Some checks failed
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
* #20383 clear rack face if no rack on edit

* #20383 clear rack face if no rack on edit

* review changes

* review changes
2026-01-23 12:26:27 -05:00
9 changed files with 93 additions and 11 deletions

View File

@@ -9,6 +9,7 @@ from django.db import connection, models
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.context import object_types_cache
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.string import title
@@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager):
"""
from netbox.models.features import get_model_features, model_is_public
# Check the request cache before hitting the database
cache = object_types_cache.get()
if cache is not None:
if ot := cache.get((model._meta.model, for_concrete_model)):
return ot
# TODO: Remove this in NetBox v5.0
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
# fall back to ContentType.
@@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager):
features=get_model_features(model),
)[0]
# Populate the request cache to avoid redundant lookups
if cache is not None:
cache[(model._meta.model, for_concrete_model)] = ot
return ot
def get_for_models(self, *models, for_concrete_models=True):

View File

@@ -20,7 +20,9 @@ from utilities.forms.fields import (
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.forms.widgets import (
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
)
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup
@@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
},
)
)
face = forms.ChoiceField(
label=_('Face'),
choices=add_blank_choice(DeviceFaceChoices),
required=False,
widget=ClearableSelect(
requires_fields=['rack']
)
)
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),

View File

@@ -3,8 +3,10 @@ from contextvars import ContextVar
__all__ = (
'current_request',
'events_queue',
'object_types_cache',
)
current_request = ContextVar('current_request', default=None)
events_queue = ContextVar('events_queue', default=dict())
object_types_cache = ContextVar('object_types_cache', default=None)

View File

@@ -1,6 +1,6 @@
from contextlib import contextmanager
from netbox.context import current_request, events_queue
from netbox.context import current_request, events_queue, object_types_cache
from netbox.utils import register_request_processor
from extras.events import flush_events
@@ -16,6 +16,7 @@ def event_tracking(request):
"""
current_request.set(request)
events_queue.set({})
object_types_cache.set({})
yield
@@ -26,3 +27,4 @@ def event_tracking(request):
# Clear context vars
current_request.set(None)
events_queue.set({})
object_types_cache.set(None)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
/**
* Initialize clear-field dependencies.
* When a required field is cleared, dependent fields with data-requires-fields attribute will also be cleared.
*/
export function initClearField(): void {
// Find all fields with data-requires-fields attribute
for (const field of getElements<HTMLSelectElement>('[data-requires-fields]')) {
const requiredFieldsAttr = field.getAttribute('data-requires-fields');
if (!requiredFieldsAttr) continue;
// Parse the comma-separated list of required field names
const requiredFields = requiredFieldsAttr.split(',').map(name => name.trim());
// Set up listeners for each required field
for (const requiredFieldName of requiredFields) {
const requiredField = document.querySelector<HTMLSelectElement>(
`[name="${requiredFieldName}"]`,
);
if (!requiredField) continue;
// Listen for changes on the required field
requiredField.addEventListener('change', () => {
// If required field is cleared, also clear this dependent field
if (!requiredField.value || requiredField.value === '') {
// Check if this field uses TomSelect
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
if (tomselect) {
tomselect.clear();
} else {
// Regular select field
field.value = '';
}
}
});
}
}
}

View File

@@ -1,9 +1,10 @@
import { initClearField } from './clearField';
import { initFormElements } from './elements';
import { initFilterModifiers } from './filterModifiers';
import { initSpeedSelector } from './speedSelector';
export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
func();
}
}

View File

@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
__all__ = (
'BulkEditNullBooleanSelect',
'ClearableSelect',
'ColorSelect',
'HTMXSelect',
'SelectWithPK',
@@ -28,6 +29,21 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
)
class ClearableSelect(forms.Select):
"""
A Select widget that will be automatically cleared when one or more required fields are cleared.
Args:
requires_fields: A list of field names that this field depends on. When any of these fields
are cleared, this field will also be cleared automatically via JavaScript.
"""
def __init__(self, *args, requires_fields=None, **kwargs):
super().__init__(*args, **kwargs)
if requires_fields:
self.attrs['data-requires-fields'] = ','.join(requires_fields)
class ColorSelect(forms.Select):
"""
Extends the built-in Select widget to colorize each <option>.