mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* Initial work on #13283 * Enable passing TomSelect HTML template attibutes on DynamicModelChoiceField * Merge disabled_indicator into option_attrs * Add support for annotating a numeric count on dropdown options * Annotate parent object on relevant fields * Improve rendering of color options * Improve rendering of color options * Rename option_attrs to context * Expose option context on ObjectVar for custom scripts * Document dropdown context variables
This commit is contained in:
@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
|||||||
|
|
||||||
* `model` - The model class
|
* `model` - The model class
|
||||||
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
|
||||||
|
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
|
||||||
* `null_option` - A label representing a "null" or empty choice (optional)
|
* `null_option` - A label representing a "null" or empty choice (optional)
|
||||||
|
|
||||||
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
|
||||||
@ -331,6 +332,22 @@ site = ObjectVar(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Context Variables
|
||||||
|
|
||||||
|
Custom context variables can be passed to override the default attribute names or to display additional information, such as a parent object.
|
||||||
|
|
||||||
|
| Name | Default | Description |
|
||||||
|
|---------------|-----------------|------------------------------------------------------------------------------|
|
||||||
|
| `value` | `"id"` | The attribute which contains the option's value |
|
||||||
|
| `label` | `"display"` | The attribute used as the option's human-friendly label |
|
||||||
|
| `description` | `"description"` | The attribute to use as a description |
|
||||||
|
| `depth`[^1] | `"_depth"` | The attribute which indicates an object's depth within a recursive hierarchy |
|
||||||
|
| `disabled` | -- | The attribute which, if true, signifies that the option should be disabled |
|
||||||
|
| `parent` | -- | The attribute which represents the object's parent object |
|
||||||
|
| `count`[^1] | -- | The attribute which contains a numeric count of related objects |
|
||||||
|
|
||||||
|
[^1]: The value of this attribute must be a positive integer
|
||||||
|
|
||||||
### MultiObjectVar
|
### MultiObjectVar
|
||||||
|
|
||||||
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
||||||
|
@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'manufacturer_id': '$manufacturer'
|
'manufacturer_id': '$manufacturer'
|
||||||
}
|
}
|
||||||
@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'manufacturer_id': '$manufacturer'
|
'manufacturer_id': '$manufacturer'
|
||||||
|
},
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
|
@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=term_cls._meta.verbose_name.title(),
|
label=term_cls._meta.verbose_name.title(),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'device_id': f'$termination_{cable_end}_device',
|
'device_id': f'$termination_{cable_end}_device',
|
||||||
'kind': 'physical', # Exclude virtual interfaces
|
'kind': 'physical', # Exclude virtual interfaces
|
||||||
@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=_('Power Feed'),
|
label=_('Power Feed'),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
'power_panel_id': f'$termination_{cable_end}_powerpanel',
|
||||||
}
|
}
|
||||||
@ -72,7 +76,9 @@ def get_cable_form(a_type, b_type):
|
|||||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
queryset=term_cls.objects.all(),
|
queryset=term_cls.objects.all(),
|
||||||
label=_('Side'),
|
label=_('Side'),
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||||
}
|
}
|
||||||
|
@ -426,7 +426,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
||||||
attrs={
|
attrs={
|
||||||
'disabled-indicator': 'device',
|
'ts-disabled-field': 'device',
|
||||||
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -434,6 +434,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
@ -461,6 +464,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
label=_('Virtual chassis'),
|
label=_('Virtual chassis'),
|
||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'master',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
vc_position = forms.IntegerField(
|
vc_position = forms.IntegerField(
|
||||||
@ -568,6 +574,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
|
|||||||
module_type = DynamicModelChoiceField(
|
module_type = DynamicModelChoiceField(
|
||||||
label=_('Module type'),
|
label=_('Module type'),
|
||||||
queryset=ModuleType.objects.all(),
|
queryset=ModuleType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
},
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
@ -774,7 +783,10 @@ class VCMemberSelectForm(forms.Form):
|
|||||||
class ComponentTemplateForm(forms.ModelForm):
|
class ComponentTemplateForm(forms.ModelForm):
|
||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all()
|
queryset=DeviceType.objects.all(),
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -789,12 +801,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
|||||||
device_type = DynamicModelChoiceField(
|
device_type = DynamicModelChoiceField(
|
||||||
label=_('Device type'),
|
label=_('Device type'),
|
||||||
queryset=DeviceType.objects.all().all(),
|
queryset=DeviceType.objects.all().all(),
|
||||||
required=False
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
module_type = DynamicModelChoiceField(
|
module_type = DynamicModelChoiceField(
|
||||||
label=_('Module type'),
|
label=_('Module type'),
|
||||||
queryset=ModuleType.objects.all(),
|
queryset=ModuleType.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'manufacturer',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -193,16 +193,19 @@ class ObjectVar(ScriptVariable):
|
|||||||
|
|
||||||
:param model: The NetBox model being referenced
|
:param model: The NetBox model being referenced
|
||||||
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
|
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
|
||||||
|
:param context: A custom dictionary mapping template context variables to fields, used when rendering <option>
|
||||||
|
elements within the dropdown menu (optional)
|
||||||
:param null_option: The label to use as a "null" selection option (optional)
|
:param null_option: The label to use as a "null" selection option (optional)
|
||||||
"""
|
"""
|
||||||
form_field = DynamicModelChoiceField
|
form_field = DynamicModelChoiceField
|
||||||
|
|
||||||
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs):
|
def __init__(self, model, query_params=None, context=None, null_option=None, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.field_attrs.update({
|
self.field_attrs.update({
|
||||||
'queryset': model.objects.all(),
|
'queryset': model.objects.all(),
|
||||||
'query_params': query_params,
|
'query_params': query_params,
|
||||||
|
'context': context,
|
||||||
'null_option': null_option,
|
'null_option': null_option,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -267,14 +267,20 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
|||||||
|
|
||||||
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||||
interface = DynamicModelChoiceField(
|
interface = DynamicModelChoiceField(
|
||||||
label=_('Interface'),
|
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'device',
|
||||||
|
},
|
||||||
selector=True,
|
selector=True,
|
||||||
|
label=_('Interface'),
|
||||||
)
|
)
|
||||||
vminterface = DynamicModelChoiceField(
|
vminterface = DynamicModelChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
context={
|
||||||
|
'parent': 'virtual_machine',
|
||||||
|
},
|
||||||
selector=True,
|
selector=True,
|
||||||
label=_('Interface'),
|
label=_('Interface'),
|
||||||
)
|
)
|
||||||
|
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js.map
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@ -31,6 +31,15 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Glean the REST API endpoint URL from the <select> element
|
// Glean the REST API endpoint URL from the <select> element
|
||||||
this.api_url = this.input.getAttribute('data-url') as string;
|
this.api_url = this.input.getAttribute('data-url') as string;
|
||||||
|
|
||||||
|
// Override any field names set as widget attributes
|
||||||
|
this.valueField = this.input.getAttribute('ts-value-field') || this.settings.valueField;
|
||||||
|
this.labelField = this.input.getAttribute('ts-label-field') || this.settings.labelField;
|
||||||
|
this.disabledField = this.input.getAttribute('ts-disabled-field') || this.settings.disabledField;
|
||||||
|
this.descriptionField = this.input.getAttribute('ts-description-field') || 'description';
|
||||||
|
this.depthField = this.input.getAttribute('ts-depth-field') || '_depth';
|
||||||
|
this.parentField = this.input.getAttribute('ts-parent-field') || null;
|
||||||
|
this.countField = this.input.getAttribute('ts-count-field') || null;
|
||||||
|
|
||||||
// Set the null option (if any)
|
// Set the null option (if any)
|
||||||
const nullOption = this.input.getAttribute('data-null-option');
|
const nullOption = this.input.getAttribute('data-null-option');
|
||||||
if (nullOption) {
|
if (nullOption) {
|
||||||
@ -82,10 +91,20 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Make the API request
|
// Make the API request
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(json => {
|
.then(apiData => {
|
||||||
self.loadCallback(json.results, []);
|
const results: Dict[] = apiData.results;
|
||||||
|
let options: Dict[] = []
|
||||||
|
for (let result of results) {
|
||||||
|
const option = self.getOptionFromData(result);
|
||||||
|
options.push(option);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
})
|
||||||
|
// Pass the options to the callback function
|
||||||
|
.then(options => {
|
||||||
|
self.loadCallback(options, []);
|
||||||
}).catch(()=>{
|
}).catch(()=>{
|
||||||
self.loadCallback([], []);
|
self.loadCallback([], []);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -126,6 +145,27 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
return queryString.stringifyUrl({ url, query });
|
return queryString.stringifyUrl({ url, query });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compile TomOption data from an API result
|
||||||
|
getOptionFromData(data: Dict) {
|
||||||
|
let option: Dict = {
|
||||||
|
id: data[this.valueField],
|
||||||
|
display: data[this.labelField],
|
||||||
|
depth: data[this.depthField] || null,
|
||||||
|
description: data[this.descriptionField] || null,
|
||||||
|
};
|
||||||
|
if (data[this.parentField]) {
|
||||||
|
let parent: Dict = data[this.parentField] as Dict;
|
||||||
|
option['parent'] = parent[this.labelField];
|
||||||
|
}
|
||||||
|
if (data[this.countField]) {
|
||||||
|
option['count'] = data[this.countField];
|
||||||
|
}
|
||||||
|
if (data[this.disabledField]) {
|
||||||
|
option['disabled'] = data[this.disabledField];
|
||||||
|
}
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transitional methods
|
* Transitional methods
|
||||||
*/
|
*/
|
||||||
|
@ -10,12 +10,34 @@ const MAX_OPTIONS = 100;
|
|||||||
|
|
||||||
// Render the HTML for a dropdown option
|
// Render the HTML for a dropdown option
|
||||||
function renderOption(data: TomOption, escape: typeof escape_html) {
|
function renderOption(data: TomOption, escape: typeof escape_html) {
|
||||||
// If the option has a `_depth` property, indent its label
|
let html = '<div>';
|
||||||
if (typeof data._depth === 'number' && data._depth > 0) {
|
|
||||||
return `<div>${'─'.repeat(data._depth)} ${escape(data[LABEL_FIELD])}</div>`;
|
// If the option has a `depth` property, indent its label
|
||||||
|
if (typeof data.depth === 'number' && data.depth > 0) {
|
||||||
|
html = `${html}${'─'.repeat(data.depth)} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div>${escape(data[LABEL_FIELD])}</div>`;
|
html = `${html}${escape(data[LABEL_FIELD])}`;
|
||||||
|
if (data['parent']) {
|
||||||
|
html = `${html} <span class="text-secondary">${escape(data['parent'])}</span>`;
|
||||||
|
}
|
||||||
|
if (data['count']) {
|
||||||
|
html = `${html} <span class="badge">${escape(data['count'])}</span>`;
|
||||||
|
}
|
||||||
|
if (data['description']) {
|
||||||
|
html = `${html}<br /><small class="text-secondary">${escape(data['description'])}</small>`;
|
||||||
|
}
|
||||||
|
html = `${html}</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the HTML for a selected item
|
||||||
|
function renderItem(data: TomOption, escape: typeof escape_html) {
|
||||||
|
if (data['parent']) {
|
||||||
|
return `<div>${escape(data['parent'])} > ${escape(data[LABEL_FIELD])}</div>`;
|
||||||
|
}
|
||||||
|
return `<div>${escape(data[LABEL_FIELD])}<div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize <select> elements which are populated via a REST API call
|
// Initialize <select> elements which are populated via a REST API call
|
||||||
@ -30,16 +52,13 @@ export function initDynamicSelects(): void {
|
|||||||
// Disable local search (search is performed on the backend)
|
// Disable local search (search is performed on the backend)
|
||||||
searchField: [],
|
searchField: [],
|
||||||
|
|
||||||
// Reference the disabled-indicator attr on the <select> element to determine
|
|
||||||
// the name of the attribute which indicates whether an option should be disabled
|
|
||||||
disabledField: select.getAttribute('disabled-indicator') || undefined,
|
|
||||||
|
|
||||||
// Load options from API immediately on focus
|
// Load options from API immediately on focus
|
||||||
preload: 'focus',
|
preload: 'focus',
|
||||||
|
|
||||||
// Define custom rendering functions
|
// Define custom rendering functions
|
||||||
render: {
|
render: {
|
||||||
option: renderOption,
|
option: renderOption,
|
||||||
|
item: renderItem,
|
||||||
},
|
},
|
||||||
|
|
||||||
// By default, load() will be called only if query.length > 0
|
// By default, load() will be called only if query.length > 0
|
||||||
|
@ -17,13 +17,18 @@ export function initStaticSelects(): void {
|
|||||||
|
|
||||||
// Initialize color selection fields
|
// Initialize color selection fields
|
||||||
export function initColorSelects(): void {
|
export function initColorSelects(): void {
|
||||||
|
function renderColor(item: TomOption, escape: typeof escape_html) {
|
||||||
|
return `<div><span class="dropdown-item-indicator color-label" style="background-color: #${escape(
|
||||||
|
item.value,
|
||||||
|
)}"></span> ${escape(item.text)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
for (const select of getElements<HTMLSelectElement>('select.color-select')) {
|
for (const select of getElements<HTMLSelectElement>('select.color-select')) {
|
||||||
new TomSelect(select, {
|
new TomSelect(select, {
|
||||||
...config,
|
...config,
|
||||||
render: {
|
render: {
|
||||||
option: function (item: TomOption, escape: typeof escape_html) {
|
option: renderColor,
|
||||||
return `<div style="background-color: #${escape(item.value)}">${escape(item.text)}</div>`;
|
item: renderColor,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -63,8 +63,19 @@ class DynamicModelChoiceMixin:
|
|||||||
initial_params: A dictionary of child field references to use for selecting a parent field's initial value
|
initial_params: A dictionary of child field references to use for selecting a parent field's initial value
|
||||||
null_option: The string used to represent a null selection (if any)
|
null_option: The string used to represent a null selection (if any)
|
||||||
disabled_indicator: The name of the field which, if populated, will disable selection of the
|
disabled_indicator: The name of the field which, if populated, will disable selection of the
|
||||||
choice (optional)
|
choice (DEPRECATED: pass `context={'disabled': '$fieldname'}` instead)
|
||||||
|
context: A mapping of <option> template variables to their API data keys (optional; see below)
|
||||||
selector: Include an advanced object selection widget to assist the user in identifying the desired object
|
selector: Include an advanced object selection widget to assist the user in identifying the desired object
|
||||||
|
|
||||||
|
Context keys:
|
||||||
|
value: The name of the attribute which contains the option's value (default: 'id')
|
||||||
|
label: The name of the attribute used as the option's human-friendly label (default: 'display')
|
||||||
|
description: The name of the attribute to use as a description (default: 'description')
|
||||||
|
depth: The name of the attribute which indicates an object's depth within a recursive hierarchy; must be a
|
||||||
|
positive integer (default: '_depth')
|
||||||
|
disabled: The name of the attribute which, if true, signifies that the option should be disabled
|
||||||
|
parent: The name of the attribute which represents the object's parent object (e.g. device for an interface)
|
||||||
|
count: The name of the attribute which contains a numeric count of related objects
|
||||||
"""
|
"""
|
||||||
filter = django_filters.ModelChoiceFilter
|
filter = django_filters.ModelChoiceFilter
|
||||||
widget = widgets.APISelect
|
widget = widgets.APISelect
|
||||||
@ -77,6 +88,7 @@ class DynamicModelChoiceMixin:
|
|||||||
initial_params=None,
|
initial_params=None,
|
||||||
null_option=None,
|
null_option=None,
|
||||||
disabled_indicator=None,
|
disabled_indicator=None,
|
||||||
|
context=None,
|
||||||
selector=False,
|
selector=False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
@ -85,6 +97,7 @@ class DynamicModelChoiceMixin:
|
|||||||
self.initial_params = initial_params or {}
|
self.initial_params = initial_params or {}
|
||||||
self.null_option = null_option
|
self.null_option = null_option
|
||||||
self.disabled_indicator = disabled_indicator
|
self.disabled_indicator = disabled_indicator
|
||||||
|
self.context = context or {}
|
||||||
self.selector = selector
|
self.selector = selector
|
||||||
|
|
||||||
super().__init__(queryset, **kwargs)
|
super().__init__(queryset, **kwargs)
|
||||||
@ -96,12 +109,17 @@ class DynamicModelChoiceMixin:
|
|||||||
if self.null_option is not None:
|
if self.null_option is not None:
|
||||||
attrs['data-null-option'] = self.null_option
|
attrs['data-null-option'] = self.null_option
|
||||||
|
|
||||||
# Set the disabled indicator, if any
|
# Set any custom template attributes for TomSelect
|
||||||
|
for var, accessor in self.context.items():
|
||||||
|
attrs[f'ts-{var}-field'] = accessor
|
||||||
|
|
||||||
|
# TODO: Remove in v4.1
|
||||||
|
# Legacy means of specifying the disabled indicator
|
||||||
if self.disabled_indicator is not None:
|
if self.disabled_indicator is not None:
|
||||||
attrs['disabled-indicator'] = self.disabled_indicator
|
attrs['ts-disabled-field'] = self.disabled_indicator
|
||||||
|
|
||||||
# Attach any static query parameters
|
# Attach any static query parameters
|
||||||
if (len(self.query_params) > 0):
|
if len(self.query_params) > 0:
|
||||||
widget.add_query_params(self.query_params)
|
widget.add_query_params(self.query_params)
|
||||||
|
|
||||||
# Include object selector?
|
# Include object selector?
|
||||||
|
@ -108,7 +108,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
|||||||
'kind': 'wireless',
|
'kind': 'wireless',
|
||||||
'device_id': '$device_a',
|
'device_id': '$device_a',
|
||||||
},
|
},
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
label=_('Interface')
|
label=_('Interface')
|
||||||
)
|
)
|
||||||
site_b = DynamicModelChoiceField(
|
site_b = DynamicModelChoiceField(
|
||||||
@ -148,7 +150,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
|||||||
'kind': 'wireless',
|
'kind': 'wireless',
|
||||||
'device_id': '$device_b',
|
'device_id': '$device_b',
|
||||||
},
|
},
|
||||||
disabled_indicator='_occupied',
|
context={
|
||||||
|
'disabled': '_occupied',
|
||||||
|
},
|
||||||
label=_('Interface')
|
label=_('Interface')
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
Reference in New Issue
Block a user