Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #16782: Add object filtering for custom fields #16994

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/customization/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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

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`).
Expand Down
7 changes: 7 additions & 0 deletions docs/models/extras/customfield.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### 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

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.)
Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/api/serializers_/customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Meta:
fields = [
'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',
'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',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class CustomFieldForm(forms.ModelForm):
FieldSet(
'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(
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
),
Expand Down
18 changes: 18 additions & 0 deletions netbox/extras/migrations/0120_customfield_related_object_filter.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
23 changes: 22 additions & 1 deletion netbox/extras/models/customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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").'
)
)
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(
default=100,
verbose_name=_('display weight'),
Expand Down Expand Up @@ -373,6 +381,17 @@ def clean(self):
.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):
"""
Prepare a value for storage as JSON data.
Expand Down Expand Up @@ -511,7 +530,8 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil
field = field_class(
queryset=model.objects.all(),
required=required,
initial=initial
initial=initial,
query_params=self.related_object_filter
)

# Multiple objects
Expand All @@ -522,6 +542,7 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil
queryset=model.objects.all(),
required=required,
initial=initial,
query_params=self.related_object_filter
)

# Text
Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet
ignore_fields = ('default',)
ignore_fields = ('default', 'related_object_filter')

@classmethod
def setUpTestData(cls):
Expand Down
8 changes: 8 additions & 0 deletions netbox/templates/extras/customfield.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ <h5 class="card-header">{% trans "Custom Field" %}</h5>
<th scope="row">{% trans "Default Value" %}</th>
<td>{{ object.default }}</td>
</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>
</div>
<div class="card">
Expand Down