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 `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 `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 `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
|
### 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
|
* [#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
|
* [#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
|
* [#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
|
* [#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
|
* [#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
|
* [#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')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
# Check custom field data on new instance
|
# Check custom field data on new instance
|
||||||
site.cf['foo'] = 'abc'
|
site.custom_field_data['foo'] = 'abc'
|
||||||
self.assertEqual(site.cf['foo'], 'abc')
|
self.assertEqual(site.cf['foo'], 'abc')
|
||||||
|
|
||||||
# Check custom field data from database
|
# Check custom field data from database
|
||||||
@ -1037,12 +1037,12 @@ class CustomFieldModelTest(TestCase):
|
|||||||
site = Site(name='Test Site', slug='test-site')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
# Set custom field data
|
# Set custom field data
|
||||||
site.cf['foo'] = 'abc'
|
site.custom_field_data['foo'] = 'abc'
|
||||||
site.cf['bar'] = 'def'
|
site.custom_field_data['bar'] = 'def'
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
del site.cf['bar']
|
del site.custom_field_data['bar']
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
def test_missing_required_field(self):
|
def test_missing_required_field(self):
|
||||||
@ -1056,11 +1056,11 @@ class CustomFieldModelTest(TestCase):
|
|||||||
site = Site(name='Test Site', slug='test-site')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
# Set custom field data with a required field omitted
|
# Set custom field data with a required field omitted
|
||||||
site.cf['foo'] = 'abc'
|
site.custom_field_data['foo'] = 'abc'
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
site.cf['baz'] = 'def'
|
site.custom_field_data['baz'] = 'def'
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.db.models.signals import class_prepared
|
from django.db.models.signals import class_prepared
|
||||||
@ -133,18 +134,35 @@ class CustomFieldsMixin(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def cf(self):
|
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
|
```python
|
||||||
>>> tenant = Tenant.objects.first()
|
>>> tenant = Tenant.objects.first()
|
||||||
>>> tenant.cf
|
>>> 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):
|
def get_custom_fields(self, omit_hidden=False):
|
||||||
"""
|
"""
|
||||||
@ -155,10 +173,13 @@ class CustomFieldsMixin(models.Model):
|
|||||||
>>> tenant.get_custom_fields()
|
>>> tenant.get_custom_fields()
|
||||||
{<CustomField: Customer ID>: 'CYB01'}
|
{<CustomField: Customer ID>: 'CYB01'}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
omit_hidden: If True, custom fields with no UI visibility will be omitted.
|
||||||
"""
|
"""
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
for field in CustomField.objects.get_for_model(self):
|
for field in CustomField.objects.get_for_model(self):
|
||||||
# Skip fields that are hidden if 'omit_hidden' is set
|
# Skip fields that are hidden if 'omit_hidden' is set
|
||||||
if omit_hidden and field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
|
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):
|
def get_custom_fields_by_group(self):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of custom field/value mappings organized by group. Hidden fields are omitted.
|
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):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
@ -82,7 +82,7 @@ class SearchIndex:
|
|||||||
# Capture custom fields
|
# Capture custom fields
|
||||||
if getattr(instance, 'custom_field_data', None):
|
if getattr(instance, 'custom_field_data', None):
|
||||||
if custom_fields is None:
|
if custom_fields is None:
|
||||||
custom_fields = instance.get_custom_fields().keys()
|
custom_fields = instance.custom_fields
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
type_ = cf.search_type
|
type_ = cf.search_type
|
||||||
value = instance.custom_field_data.get(cf.name)
|
value = instance.custom_field_data.get(cf.name)
|
||||||
|
Reference in New Issue
Block a user