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

Closes #15357: Rename CustomField.object_type to related_object_type (#15366)

This commit is contained in:
Jeremy Stretch
2024-03-09 06:16:17 -05:00
committed by GitHub
parent 663bd32464
commit 78dd65219f
15 changed files with 97 additions and 44 deletions

View File

@ -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` | | 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` | | 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. For object and multiple-object fields only. Designates the type of NetBox object being referenced.

View File

@ -57,10 +57,10 @@ class CustomFieldsDataField(Field):
for cf in self._get_custom_fields(): for cf in self._get_custom_fields():
value = cf.deserialize(obj.get(cf.name)) value = cf.deserialize(obj.get(cf.name))
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: 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 value = serializer(value, nested=True, context=self.parent.context).data
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: 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 value = serializer(value, nested=True, many=True, context=self.parent.context).data
data[cf.name] = value data[cf.name] = value
@ -79,7 +79,7 @@ class CustomFieldsDataField(Field):
CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT 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 many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
if serializer.is_valid(): if serializer.is_valid():

View File

@ -44,7 +44,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
many=True many=True
) )
type = ChoiceField(choices=CustomFieldTypeChoices) type = ChoiceField(choices=CustomFieldTypeChoices)
object_type = ContentTypeField( related_object_type = ContentTypeField(
queryset=ObjectType.objects.all(), queryset=ObjectType.objects.all(),
required=False, required=False,
allow_null=True allow_null=True
@ -62,10 +62,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = [
'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', 'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'created', 'last_updated', 'choice_set', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -132,6 +132,10 @@ class CustomFieldFilterSet(BaseFilterSet):
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' 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( choice_set_id = django_filters.ModelMultipleChoiceFilter(
queryset=CustomFieldChoiceSet.objects.all() queryset=CustomFieldChoiceSet.objects.all()
) )

View File

@ -40,7 +40,7 @@ class CustomFieldImportForm(CSVModelForm):
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
help_text=_('Field data type (e.g. text, integer, etc.)') help_text=_('Field data type (e.g. text, integer, etc.)')
) )
object_type = CSVContentTypeField( related_object_type = CSVContentTypeField(
label=_('Object type'), label=_('Object type'),
queryset=ObjectType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
class Meta: class Meta:
model = CustomField model = CustomField
fields = ( 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', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
) )

View File

@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ( (_('Attributes'), (
'type', 'object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'is_cloneable', 'ui_editable', 'is_cloneable',
)), )),
) )
object_type_id = ContentTypeMultipleChoiceField( related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
required=False, required=False,
label=_('Object type') label=_('Related object type')
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,

View File

@ -42,8 +42,8 @@ class CustomFieldForm(forms.ModelForm):
label=_('Object types'), label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_fields') queryset=ObjectType.objects.with_feature('custom_fields')
) )
object_type = ContentTypeChoiceField( related_object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Related object type'),
queryset=ObjectType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
help_text=_("Type of the related object (for object/multi-object fields only)") help_text=_("Type of the related object (for object/multi-object fields only)")
@ -55,7 +55,7 @@ class CustomFieldForm(forms.ModelForm):
fieldsets = ( fieldsets = (
(_('Custom Field'), ( (_('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')), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')), (_('Values'), ('default', 'choice_set')),

View File

@ -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',
),
]

View File

@ -78,7 +78,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
default=CustomFieldTypeChoices.TYPE_TEXT, default=CustomFieldTypeChoices.TYPE_TEXT,
help_text=_('The type of data this custom field holds') help_text=_('The type of data this custom field holds')
) )
object_type = models.ForeignKey( related_object_type = models.ForeignKey(
to='core.ObjectType', to='core.ObjectType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
objects = CustomFieldManager() objects = CustomFieldManager()
clone_fields = ( 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', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
) )
@ -344,11 +344,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object fields must define an object_type; other fields must not # Object fields must define an object_type; other fields must not
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.object_type: if not self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _("Object fields must define an object type.") 'object_type': _("Object fields must define an object type.")
}) })
elif self.object_type: elif self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _( 'object_type': _(
"{type} fields may not define an object type.") "{type} fields may not define an object type.")
@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
except ValueError: except ValueError:
return value return value
if self.type == CustomFieldTypeChoices.TYPE_OBJECT: 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() return model.objects.filter(pk=value).first()
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: 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 model.objects.filter(pk__in=value)
return value return value
@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_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_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),
@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Multiple objects # Multiple objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: 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_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),

View File

@ -57,6 +57,9 @@ class CustomFieldTable(NetBoxTable):
description = columns.MarkdownColumn( description = columns.MarkdownColumn(
verbose_name=_('Description') verbose_name=_('Description')
) )
related_object_type = columns.ContentTypeColumn(
verbose_name=_('Related Object Type')
)
choice_set = tables.Column( choice_set = tables.Column(
linkify=True, linkify=True,
verbose_name=_('Choice Set') verbose_name=_('Choice Set')
@ -73,9 +76,9 @@ class CustomFieldTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'choices', 'created', 'last_updated', 'weight', 'choice_set', 'choices', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')

View File

@ -350,7 +350,7 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.object_types.set([self.object_type]) cf.object_types.set([self.object_type])
@ -382,7 +382,7 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.object_types.set([self.object_type]) cf.object_types.set([self.object_type])
@ -498,16 +498,28 @@ class CustomFieldTest(TestCase):
).full_clean() ).full_clean()
# Object # Object
CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() CustomField(
with self.assertRaises(ValidationError): name='test',
CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() 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 # Multi-object
CustomField( CustomField(
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=[site.pk] default=[site.pk]
).full_clean() ).full_clean()
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -515,7 +527,7 @@ class CustomFieldTest(TestCase):
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=["xxx"] default=["xxx"]
).full_clean() ).full_clean()
@ -581,13 +593,13 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
name='object_field', name='object_field',
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
default=vlans[0].pk, default=vlans[0].pk,
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
name='multiobject_field', 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], default=[vlans[0].pk, vlans[1].pk],
), ),
) )
@ -1410,7 +1422,7 @@ class CustomFieldModelFilterTest(TestCase):
cf = CustomField( cf = CustomField(
name='cf11', name='cf11',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ObjectType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.object_types.set([object_type]) cf.object_types.set([object_type])
@ -1419,7 +1431,7 @@ class CustomFieldModelFilterTest(TestCase):
cf = CustomField( cf = CustomField(
name='cf12', name='cf12',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ObjectType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.object_types.set([object_type]) cf.object_types.set([object_type])

View File

@ -86,6 +86,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
ui_editable=CustomFieldUIEditableChoices.HIDDEN, ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1] 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) CustomField.objects.bulk_create(custom_fields)
custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
@ -108,6 +118,12 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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): def test_required(self):
params = {'required': True} params = {'required': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)

View File

@ -62,14 +62,14 @@ class CustomFieldModelFormTest(TestCase):
cf_object = CustomField.objects.create( cf_object = CustomField.objects.create(
name='object', name='object',
type=CustomFieldTypeChoices.TYPE_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_object.object_types.set([object_type])
cf_multiobject = CustomField.objects.create( cf_multiobject = CustomField.objects.create(
name='multiobject', name='multiobject',
type=CustomFieldTypeChoices.TYPE_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]) cf_multiobject.object_types.set([object_type])

View File

@ -54,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( 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', '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', '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', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',

View File

@ -17,7 +17,9 @@
<th scope="row">Type</th> <th scope="row">Type</th>
<td> <td>
{{ object.get_type_display }} {{ 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 %}
</td> </td>
</tr> </tr>
<tr> <tr>