diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index ee1fde2b7..0d92ec656 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -82,6 +82,10 @@ The default value to populate for the custom field when creating new objects (op For choice and multi-choice custom fields only. A comma-delimited list of the available choices. +### Cloneable + +If enabled, values from this field will be automatically pre-populated when cloning existing objects. + ### Minimum Value For numeric custom fields only. The minimum valid value (optional). diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 1a353a294..ac2fab66b 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -28,6 +28,7 @@ A new ASN range model has been introduced to facilitate the provisioning of new * [#7947](https://github.com/netbox-community/netbox/issues/7947) - Enable marking IP ranges as fully utilized * [#8272](https://github.com/netbox-community/netbox/issues/8272) - Support bridge relationships among device type interfaces +* [#8749](https://github.com/netbox-community/netbox/issues/8749) - Support replicating custom field values when cloning an object * [#8958](https://github.com/netbox-community/netbox/issues/8958) - Changes in background job status can trigger webhooks * [#9073](https://github.com/netbox-community/netbox/issues/9073) - Enable syncing config context data from remote sources * [#9653](https://github.com/netbox-community/netbox/issues/9653) - Enable setting a default platform for device types diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index b952866a9..59e264ccf 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -98,8 +98,9 @@ class CustomFieldSerializer(ValidatedModelSerializer): model = CustomField fields = [ 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', - 'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default', 'weight', - 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated', + 'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default', + 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', + 'last_updated', ] def get_data_type(self, obj): diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 53898a0c9..5cef28bc5 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -78,7 +78,7 @@ class CustomFieldFilterSet(BaseFilterSet): model = CustomField fields = [ 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visibility', - 'weight', 'description', + 'weight', 'is_cloneable', 'description', ] def search(self, queryset, name, value): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 6e0cf7fc7..e976df107 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -37,7 +37,9 @@ __all__ = ( class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - ('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')), + ('Attributes', ( + 'type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility', 'is_cloneable', + )), ) content_type_id = ContentTypeMultipleChoiceField( queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()), @@ -66,6 +68,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): required=False, label=_('UI visibility') ) + is_cloneable = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) class JobResultFilterForm(SavedFiltersMixin, FilterForm): diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index af3e9187e..2a3adf790 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -47,7 +47,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): ('Custom Field', ( 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', )), - ('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight')), + ('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')), ('Values', ('default', 'choices')), ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), ) diff --git a/netbox/extras/migrations/0085_customfield_is_cloneable.py b/netbox/extras/migrations/0089_customfield_is_cloneable.py similarity index 88% rename from netbox/extras/migrations/0085_customfield_is_cloneable.py rename to netbox/extras/migrations/0089_customfield_is_cloneable.py index 79927f3e1..7f577b45a 100644 --- a/netbox/extras/migrations/0085_customfield_is_cloneable.py +++ b/netbox/extras/migrations/0089_customfield_is_cloneable.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('extras', '0084_staging'), + ('extras', '0088_jobresult_webhooks'), ] operations = [ diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 0f97d2a74..998ec5b4b 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -29,12 +29,14 @@ class CustomFieldTable(NetBoxTable): content_types = columns.ContentTypesColumn() required = columns.BooleanColumn() ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility") + is_cloneable = columns.BooleanColumn() class Meta(NetBoxTable.Meta): model = CustomField fields = ( 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', - 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choices', 'created', 'last_updated', + 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choices', 'created', + 'last_updated', ) default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 0283e5ede..31f911dff 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -121,14 +121,11 @@ class CloningMixin(models.Model): if is_taggable(self): attrs['tags'] = [tag.pk for tag in self.tags.all()] - # check custom fields + # Include any cloneable custom fields if hasattr(self, 'custom_field_data'): - from extras.models import CustomField - - for field in CustomField.objects.get_for_model(self): + for field in self.get_custom_fields(): if field.is_cloneable: - value = self.custom_field_data.get(field.name) - attrs[f'cf_{field.name}'] = field.deserialize(value) + attrs[f'cf_{field.name}'] = self.custom_field_data.get(field.name) return attrs