From 380ac5c7dcc0ce6cd3ae63c15b312175a4c33568 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Mar 2024 11:37:17 -0500 Subject: [PATCH] Closes #15357: Rename CustomField.object_type to related_object_type --- docs/models/extras/customfield.md | 2 +- netbox/extras/api/customfields.py | 6 ++-- .../extras/api/serializers_/customfields.py | 10 +++--- netbox/extras/filtersets.py | 4 +++ netbox/extras/forms/bulk_import.py | 4 +-- netbox/extras/forms/filtersets.py | 8 ++--- netbox/extras/forms/model_forms.py | 6 ++-- .../0113_customfield_rename_object_type.py | 16 +++++++++ netbox/extras/models/customfields.py | 16 ++++----- netbox/extras/tables/tables.py | 9 +++-- netbox/extras/tests/test_customfields.py | 34 +++++++++++++------ netbox/extras/tests/test_filtersets.py | 16 +++++++++ netbox/extras/tests/test_forms.py | 4 +-- netbox/extras/tests/test_views.py | 2 +- netbox/templates/extras/customfield.html | 4 ++- 15 files changed, 97 insertions(+), 44 deletions(-) create mode 100644 netbox/extras/migrations/0113_customfield_rename_object_type.py diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index e68ddb79d3..495c4e2e83 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following: | Object | A single NetBox object of the type defined by `object_type` | | Multiple object | One or more NetBox objects of the type defined by `object_type` | -### Object Type +### Related Object Type For object and multiple-object fields only. Designates the type of NetBox object being referenced. diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 81535a1476..09f2479294 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -57,10 +57,10 @@ def to_representation(self, obj): for cf in self._get_custom_fields(): value = cf.deserialize(obj.get(cf.name)) if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class()) + serializer = get_serializer_for_model(cf.related_object_type.model_class()) value = serializer(value, nested=True, context=self.parent.context).data elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class()) + serializer = get_serializer_for_model(cf.related_object_type.model_class()) value = serializer(value, nested=True, many=True, context=self.parent.context).data data[cf.name] = value @@ -79,7 +79,7 @@ def to_internal_value(self, data): CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT ): - serializer_class = get_serializer_for_model(cf.object_type.model_class()) + serializer_class = get_serializer_for_model(cf.related_object_type.model_class()) many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) if serializer.is_valid(): diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py index efd6db063f..79bb395578 100644 --- a/netbox/extras/api/serializers_/customfields.py +++ b/netbox/extras/api/serializers_/customfields.py @@ -44,7 +44,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): many=True ) type = ChoiceField(choices=CustomFieldTypeChoices) - object_type = ContentTypeField( + related_object_type = ContentTypeField( queryset=ObjectType.objects.all(), required=False, allow_null=True @@ -62,10 +62,10 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'object_types', 'type', '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', 'validation_regex', 'choice_set', - 'created', 'last_updated', + 'id', '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', 'validation_regex', + 'choice_set', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 6cb3095804..d88b8c9b30 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -132,6 +132,10 @@ class CustomFieldFilterSet(BaseFilterSet): object_type = ContentTypeFilter( field_name='object_types' ) + related_object_type_id = MultiValueNumberFilter( + field_name='related_object_type__id' + ) + related_object_type = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 39d2933a7b..55f71dbd21 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -40,7 +40,7 @@ class CustomFieldImportForm(CSVModelForm): choices=CustomFieldTypeChoices, help_text=_('Field data type (e.g. text, integer, etc.)') ) - object_type = CSVContentTypeField( + related_object_type = CSVContentTypeField( label=_('Object type'), queryset=ObjectType.objects.public(), required=False, @@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'group_name', 'type', 'object_types', 'object_type', 'required', 'description', + 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 285e7618fe..73751872ff 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', - 'is_cloneable', + 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', + 'ui_editable', 'is_cloneable', )), ) - object_type_id = ContentTypeMultipleChoiceField( + related_object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, - label=_('Object type') + label=_('Related object type') ) type = forms.MultipleChoiceField( choices=CustomFieldTypeChoices, diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 7f36db6572..09d2d95357 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -42,8 +42,8 @@ class CustomFieldForm(forms.ModelForm): label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields') ) - object_type = ContentTypeChoiceField( - label=_('Object type'), + related_object_type = ContentTypeChoiceField( + label=_('Related object type'), queryset=ObjectType.objects.public(), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") @@ -55,7 +55,7 @@ class CustomFieldForm(forms.ModelForm): fieldsets = ( (_('Custom Field'), ( - 'object_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', + 'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description', )), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), (_('Values'), ('default', 'choice_set')), diff --git a/netbox/extras/migrations/0113_customfield_rename_object_type.py b/netbox/extras/migrations/0113_customfield_rename_object_type.py new file mode 100644 index 0000000000..73c4a2a61e --- /dev/null +++ b/netbox/extras/migrations/0113_customfield_rename_object_type.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0112_tag_update_object_types'), + ] + + operations = [ + migrations.RenameField( + model_name='customfield', + old_name='object_type', + new_name='related_object_type', + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 681bd4f2a6..a14c71c636 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -78,7 +78,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): default=CustomFieldTypeChoices.TYPE_TEXT, help_text=_('The type of data this custom field holds') ) - object_type = models.ForeignKey( + related_object_type = models.ForeignKey( to='core.ObjectType', on_delete=models.PROTECT, blank=True, @@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): objects = CustomFieldManager() clone_fields = ( - 'object_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', + 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', ) @@ -344,11 +344,11 @@ def clean(self): # Object fields must define an object_type; other fields must not if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): - if not self.object_type: + if not self.related_object_type: raise ValidationError({ 'object_type': _("Object fields must define an object type.") }) - elif self.object_type: + elif self.related_object_type: raise ValidationError({ 'object_type': _( "{type} fields may not define an object type.") @@ -388,10 +388,10 @@ def deserialize(self, value): except ValueError: return value if self.type == CustomFieldTypeChoices.TYPE_OBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() return model.objects.filter(pk=value).first() if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() return model.objects.filter(pk__in=value) return value @@ -488,7 +488,7 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil # Object elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField field = field_class( queryset=model.objects.all(), @@ -498,7 +498,7 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil # Multiple objects elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField field = field_class( queryset=model.objects.all(), diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index fee0c9f292..0911d2049b 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -55,6 +55,9 @@ class CustomFieldTable(NetBoxTable): description = columns.MarkdownColumn( verbose_name=_('Description') ) + related_object_type = columns.ContentTypeColumn( + verbose_name=_('Related Object Type') + ) choice_set = tables.Column( linkify=True, verbose_name=_('Choice Set') @@ -71,9 +74,9 @@ class CustomFieldTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomField fields = ( - 'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', - 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', - 'choices', 'created', 'last_updated', + 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required', + 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', + 'weight', 'choice_set', 'choices', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 7ca18250c9..0c8b86f93d 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -350,7 +350,7 @@ def test_object_field(self): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), required=False ) cf.object_types.set([self.object_type]) @@ -382,7 +382,7 @@ def test_multiobject_field(self): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), required=False ) cf.object_types.set([self.object_type]) @@ -498,16 +498,28 @@ def test_default_value_validation(self): ).full_clean() # Object - CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() - with self.assertRaises(ValidationError): - CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() + CustomField( + name='test', + type='object', + required=True, + related_object_type=object_type, + default=site.pk + ).full_clean() + with (self.assertRaises(ValidationError)): + CustomField( + name='test', + type='object', + required=True, + related_object_type=object_type, + default="xxx" + ).full_clean() # Multi-object CustomField( name='test', type='multiobject', required=True, - object_type=object_type, + related_object_type=object_type, default=[site.pk] ).full_clean() with self.assertRaises(ValidationError): @@ -515,7 +527,7 @@ def test_default_value_validation(self): name='test', type='multiobject', required=True, - object_type=object_type, + related_object_type=object_type, default=["xxx"] ).full_clean() @@ -581,13 +593,13 @@ def setUpTestData(cls): CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), default=vlans[0].pk, ), CustomField( type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, name='multiobject_field', - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), ) @@ -1410,7 +1422,7 @@ def setUpTestData(cls): cf = CustomField( name='cf11', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(Manufacturer) + related_object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() cf.object_types.set([object_type]) @@ -1419,7 +1431,7 @@ def setUpTestData(cls): cf = CustomField( name='cf12', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(Manufacturer) + related_object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() cf.object_types.set([object_type]) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 4f92798310..bec62c6887 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -86,6 +86,16 @@ def setUpTestData(cls): ui_editable=CustomFieldUIEditableChoices.HIDDEN, choice_set=choice_sets[1] ), + CustomField( + name='Custom Field 6', + type=CustomFieldTypeChoices.TYPE_OBJECT, + related_object_type=ObjectType.objects.get_by_natural_key('dcim', 'site'), + required=False, + weight=600, + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED, + ui_visible=CustomFieldUIVisibleChoices.HIDDEN, + ui_editable=CustomFieldUIEditableChoices.HIDDEN + ), ) CustomField.objects.bulk_create(custom_fields) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) @@ -108,6 +118,12 @@ def test_object_type(self): params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_related_object_type(self): + params = {'related_object_type': 'dcim.site'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_required(self): params = {'required': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 4c96e72d60..66c4e245e5 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -62,14 +62,14 @@ def setUpTestData(cls): cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(Site) + related_object_type=ObjectType.objects.get_for_model(Site) ) cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(Site) + related_object_type=ObjectType.objects.get_for_model(Site) ) cf_multiobject.object_types.set([object_type]) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index ca6ad9174a..fd478acd42 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -54,7 +54,7 @@ def setUpTestData(cls): } cls.csv_data = ( - 'name,label,type,object_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', + 'name,label,type,object_types,related_object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index ddc6b30f41..1fec35417d 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -17,7 +17,9 @@
{% trans "Custom Field" %}
Type {{ object.get_type_display }} - {% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %} + {% if object.related_object_type %} + ({{ object.related_object_type.model|bettertitle }}) + {% endif %}