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:
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user