mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
* Fixes #16782: Add object filtering for custom fields * Add validation for related_object_filter * Extend documentation & misc cleanup --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
a12fdd6e2d
commit
650898719e
@ -74,6 +74,8 @@ If a default value is specified for a selection field, it must exactly match one
|
|||||||
|
|
||||||
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.
|
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.
|
||||||
|
|
||||||
|
By default, an object choice field will make all objects of that type available for selection in the drop-down. The list choices can be filtered to show only objects with certain values by providing a `query_params` dict in the Related Object Filter field, as a JSON value. More information about `query_params` can be found [here](./custom-scripts.md#objectvar).
|
||||||
|
|
||||||
## Custom Fields in Templates
|
## Custom Fields in Templates
|
||||||
|
|
||||||
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
|
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
|
||||||
|
@ -42,6 +42,13 @@ The type of data this field holds. This must be one of the following:
|
|||||||
|
|
||||||
For object and multiple-object fields only. Designates the type of NetBox object being referenced.
|
For object and multiple-object fields only. Designates the type of NetBox object being referenced.
|
||||||
|
|
||||||
|
### Related Object Filter
|
||||||
|
|
||||||
|
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
This setting is employed for convenience only, and should not be relied upon to enforce data integrity.
|
||||||
|
|
||||||
### Weight
|
### Weight
|
||||||
|
|
||||||
A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.)
|
A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.)
|
||||||
|
@ -62,7 +62,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
|
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
|
||||||
'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
||||||
'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight', 'validation_minimum', 'validation_maximum',
|
||||||
'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
|
'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
@ -67,7 +67,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
FieldSet(
|
FieldSet(
|
||||||
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
|
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
|
||||||
),
|
),
|
||||||
FieldSet('default', 'choice_set', name=_('Values')),
|
FieldSet('default', 'choice_set', 'related_object_filter', name=_('Values')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.7 on 2024-07-26 01:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0119_eventrule_event_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='related_object_filter',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -154,6 +154,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").'
|
'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
related_object_filter = models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text=_(
|
||||||
|
'Filter the object selection choices using a query_params dict (must be a JSON value).'
|
||||||
|
'Encapsulate strings with double quotes (e.g. "Foo").'
|
||||||
|
)
|
||||||
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
verbose_name=_('display weight'),
|
verbose_name=_('display weight'),
|
||||||
@ -373,6 +381,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
.format(type=self.get_type_display())
|
.format(type=self.get_type_display())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Related object filter can be set only for object-type fields, and must contain a dictionary mapping (if set)
|
||||||
|
if self.related_object_filter is not None:
|
||||||
|
if self.type not in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||||
|
raise ValidationError({
|
||||||
|
'related_object_filter': _("A related object filter can be defined only for object fields.")
|
||||||
|
})
|
||||||
|
if type(self.related_object_filter) is not dict:
|
||||||
|
raise ValidationError({
|
||||||
|
'related_object_filter': _("Filter must be defined as a dictionary mapping attributes to values.")
|
||||||
|
})
|
||||||
|
|
||||||
def serialize(self, value):
|
def serialize(self, value):
|
||||||
"""
|
"""
|
||||||
Prepare a value for storage as JSON data.
|
Prepare a value for storage as JSON data.
|
||||||
@ -511,7 +530,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
field = field_class(
|
field = field_class(
|
||||||
queryset=model.objects.all(),
|
queryset=model.objects.all(),
|
||||||
required=required,
|
required=required,
|
||||||
initial=initial
|
initial=initial,
|
||||||
|
query_params=self.related_object_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Multiple objects
|
# Multiple objects
|
||||||
@ -522,6 +542,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
queryset=model.objects.all(),
|
queryset=model.objects.all(),
|
||||||
required=required,
|
required=required,
|
||||||
initial=initial,
|
initial=initial,
|
||||||
|
query_params=self.related_object_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
|
@ -23,7 +23,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|||||||
class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.all()
|
||||||
filterset = CustomFieldFilterSet
|
filterset = CustomFieldFilterSet
|
||||||
ignore_fields = ('default',)
|
ignore_fields = ('default', 'related_object_filter')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
@ -52,6 +52,14 @@
|
|||||||
<th scope="row">{% trans "Default Value" %}</th>
|
<th scope="row">{% trans "Default Value" %}</th>
|
||||||
<td>{{ object.default }}</td>
|
<td>{{ object.default }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Related object filter" %}</th>
|
||||||
|
{% if object.related_object_filter %}
|
||||||
|
<td><pre>{{ object.related_object_filter|json }}</pre></td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ ''|placeholder }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
Loading…
Reference in New Issue
Block a user