1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #10052: The cf attribute now returns deserialized custom field data

This commit is contained in:
jeremystretch
2022-11-04 14:53:18 -04:00
parent fe73e90b7b
commit ea6d86e6c4
4 changed files with 56 additions and 17 deletions

View File

@ -9,6 +9,7 @@
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
* The `content_type` field on the CustomLink and ExportTemplate models have been renamed to `content_types` and now supports the assignment of multiple content types.
* The `cf` property on an object with custom fields now returns deserialized values. For example, a custom field referencing an object will return the object instance rather than its numeric ID. To access the raw serialized values, use `custom_field_data` instead.
### New Features
@ -37,6 +38,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#9817](https://github.com/netbox-community/netbox/issues/9817) - Add `assigned_object` field to GraphQL type for IP addresses and L2VPN terminations
* [#9832](https://github.com/netbox-community/netbox/issues/9832) - Add `mounting_depth` field to rack model
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
* [#10052](https://github.com/netbox-community/netbox/issues/10052) - The `cf` attribute now returns deserialized custom field data
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields

View File

@ -1022,7 +1022,7 @@ class CustomFieldModelTest(TestCase):
site = Site(name='Test Site', slug='test-site')
# Check custom field data on new instance
site.cf['foo'] = 'abc'
site.custom_field_data['foo'] = 'abc'
self.assertEqual(site.cf['foo'], 'abc')
# Check custom field data from database
@ -1037,12 +1037,12 @@ class CustomFieldModelTest(TestCase):
site = Site(name='Test Site', slug='test-site')
# Set custom field data
site.cf['foo'] = 'abc'
site.cf['bar'] = 'def'
site.custom_field_data['foo'] = 'abc'
site.custom_field_data['bar'] = 'def'
with self.assertRaises(ValidationError):
site.clean()
del site.cf['bar']
del site.custom_field_data['bar']
site.clean()
def test_missing_required_field(self):
@ -1056,11 +1056,11 @@ class CustomFieldModelTest(TestCase):
site = Site(name='Test Site', slug='test-site')
# Set custom field data with a required field omitted
site.cf['foo'] = 'abc'
site.custom_field_data['foo'] = 'abc'
with self.assertRaises(ValidationError):
site.clean()
site.cf['baz'] = 'def'
site.custom_field_data['baz'] = 'def'
site.clean()

View File

@ -1,4 +1,5 @@
from collections import defaultdict
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from django.db.models.signals import class_prepared
@ -133,18 +134,35 @@ class CustomFieldsMixin(models.Model):
class Meta:
abstract = True
@property
@cached_property
def cf(self):
"""
A pass-through convenience alias for accessing `custom_field_data` (read-only).
Return a dictionary mapping each custom field for this instance to its deserialized value.
```python
>>> tenant = Tenant.objects.first()
>>> tenant.cf
{'cust_id': 'CYB01'}
{'primary_site': <Site: DM-NYC>, 'cust_id': 'DMI01', 'is_active': True}
```
"""
return self.custom_field_data
return {
cf.name: cf.deserialize(self.custom_field_data.get(cf.name))
for cf in self.custom_fields
}
@cached_property
def custom_fields(self):
"""
Return the QuerySet of CustomFields assigned to this model.
```python
>>> tenant = Tenant.objects.first()
>>> tenant.custom_fields
<RestrictedQuerySet [<CustomField: Primary site>, <CustomField: Customer ID>, <CustomField: Is active>]>
```
"""
from extras.models import CustomField
return CustomField.objects.get_for_model(self)
def get_custom_fields(self, omit_hidden=False):
"""
@ -155,10 +173,13 @@ class CustomFieldsMixin(models.Model):
>>> tenant.get_custom_fields()
{<CustomField: Customer ID>: 'CYB01'}
```
Args:
omit_hidden: If True, custom fields with no UI visibility will be omitted.
"""
from extras.models import CustomField
data = {}
for field in CustomField.objects.get_for_model(self):
# Skip fields that are hidden if 'omit_hidden' is set
if omit_hidden and field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
@ -172,12 +193,28 @@ class CustomFieldsMixin(models.Model):
def get_custom_fields_by_group(self):
"""
Return a dictionary of custom field/value mappings organized by group. Hidden fields are omitted.
"""
grouped_custom_fields = defaultdict(dict)
for cf, value in self.get_custom_fields(omit_hidden=True).items():
grouped_custom_fields[cf.group_name][cf] = value
return dict(grouped_custom_fields)
```python
>>> tenant = Tenant.objects.first()
>>> tenant.get_custom_fields_by_group()
{
'': {<CustomField: Primary site>: <Site: DM-NYC>},
'Billing': {<CustomField: Customer ID>: 'DMI01', <CustomField: Is active>: True}
}
```
"""
from extras.models import CustomField
groups = defaultdict(dict)
visible_custom_fields = CustomField.objects.get_for_model(self).exclude(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
)
for cf in visible_custom_fields:
value = self.custom_field_data.get(cf.name)
value = cf.deserialize(value)
groups[cf.group_name][cf] = value
return dict(groups)
def clean(self):
super().clean()

View File

@ -82,7 +82,7 @@ class SearchIndex:
# Capture custom fields
if getattr(instance, 'custom_field_data', None):
if custom_fields is None:
custom_fields = instance.get_custom_fields().keys()
custom_fields = instance.custom_fields
for cf in custom_fields:
type_ = cf.search_type
value = instance.custom_field_data.get(cf.name)