diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 8eb057020..1a5cc8435 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,8 +1,8 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase from circuits.models import * +from core.models import ObjectType from dcim.choices import * from dcim.models import * from extras.models import CustomField @@ -293,8 +293,8 @@ class DeviceTestCase(TestCase): # Create a CustomField with a default value & assign it to all component models cf1 = CustomField.objects.create(name='cf1', default='foo') - cf1.content_types.set( - ContentType.objects.filter(app_label='dcim', model__in=[ + cf1.object_types.set( + ObjectType.objects.filter(app_label='dcim', model__in=[ 'consoleport', 'consoleserverport', 'powerport', diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 6cd3a245e..77c3a170e 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -26,7 +26,7 @@ class CustomFieldDefaultValues: # Retrieve the CustomFields for the parent model content_type = ContentType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(content_types=content_type) + fields = CustomField.objects.filter(object_types=content_type) # Populate the default value for each CustomField value = {} @@ -48,7 +48,7 @@ class CustomFieldsDataField(Field): """ if not hasattr(self, '_custom_fields'): content_type = ContentType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(content_types=content_type) + self._custom_fields = CustomField.objects.filter(object_types=content_type) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index a87947d68..370450712 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -117,7 +117,7 @@ class WebhookSerializer(NetBoxModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_fields'), many=True ) @@ -139,7 +139,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + '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', diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 734f7db50..290670f5c 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -124,10 +124,12 @@ class CustomFieldFilterSet(BaseFilterSet): type = django_filters.MultipleChoiceFilter( choices=CustomFieldTypeChoices ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' + ) + object_types = ContentTypeFilter( + field_name='object_types' ) - content_types = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) @@ -140,7 +142,7 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField fields = [ - 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', + 'id', 'object_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', 'description', ] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 133233ccc..1d1b27617 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -30,8 +30,8 @@ __all__ = ( class CustomFieldImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields'), help_text=_("One or more assigned object types") ) @@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', + 'name', 'label', 'group_name', 'type', 'object_types', '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 075144d12..ccfeb8c1d 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,11 +38,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', + 'type', 'object_types_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'is_cloneable', )), ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 443ba0a30..776265878 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -2,7 +2,6 @@ import json import re from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -39,8 +38,8 @@ __all__ = ( class CustomFieldForm(forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields') ) object_type = ContentTypeChoiceField( @@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm): fieldsets = ( (_('Custom Field'), ( - 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', + 'object_types', 'name', 'label', 'group_name', 'type', '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/graphql/types.py b/netbox/extras/graphql/types.py index 4981ddd72..643568eaa 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,7 +39,7 @@ class CustomFieldType(ObjectType): class Meta: model = models.CustomField - exclude = ('content_types', ) + exclude = ('object_types', 'object_type') filterset_class = filtersets.CustomFieldFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py new file mode 100644 index 000000000..2b7b2358a --- /dev/null +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -0,0 +1,28 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('extras', '0110_remove_eventrule_action_parameters'), + ] + + operations = [ + migrations.RenameField( + model_name='customfield', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='customfield', + name='object_types', + field=models.ManyToManyField(related_name='custom_fields', to='core.objecttype'), + ), + migrations.AlterField( + model_name='customfield', + name='object_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 34b58e712..311ccce76 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -53,7 +53,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): Return all CustomFields assigned to the given model. """ content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) - return self.get_queryset().filter(content_types=content_type) + return self.get_queryset().filter(object_types=content_type) def get_defaults_for_model(self, model): """ @@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='custom_fields', help_text=_('The object(s) to which this field applies.') ) @@ -79,7 +79,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The type of data this custom field holds') ) object_type = models.ForeignKey( - to='contenttypes.ContentType', + to='core.ObjectType', on_delete=models.PROTECT, blank=True, null=True, @@ -284,7 +284,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ Called when a CustomField has been renamed. Updates all assigned object data. """ - for ct in self.content_types.all(): + for ct in self.object_types.all(): model = ct.model_class() params = {f'custom_field_data__{old_name}__isnull': False} instances = model.objects.filter(**params) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index f8dc204e7..85c00169c 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -205,13 +205,13 @@ def handle_cf_deleted(instance, **kwargs): """ Handle the cleanup of old custom field data when a CustomField is deleted. """ - instance.remove_stale_data(instance.content_types.all()) + instance.remove_stale_data(instance.object_types.all()) post_save.connect(handle_cf_renamed, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField) -m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through) -m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) +m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.object_types.through) +m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.object_types.through) # diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 5db906b25..01bbb312f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -7,10 +7,10 @@ from django.utils.timezone import make_aware from rest_framework import status from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from extras.choices import * from extras.models import * -from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from utilities.testing import APITestCase, APIViewTestCases @@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf4', 'type': 'date', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf5', 'type': 'url', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf6', 'type': 'text', }, @@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } update_data = { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'New_Name', 'description': 'New description', } @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_ct = ObjectType.objects.get_for_model(Site) custom_fields = ( CustomField( @@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): ) CustomField.objects.bulk_create(custom_fields) for cf in custom_fields: - cf.content_types.add(site_ct) + cf.object_types.add(site_ct) class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index e144c5dee..d9d6f1f45 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -3,6 +3,7 @@ from django.test import override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.choices import SiteStatusChoices from dcim.models import Site from extras.choices import * @@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase): ) # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model cf_select = CustomField( @@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) def test_create_object(self): tags = create_tags('Tag 1', 'Tag 2') @@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase): def setUpTestData(cls): # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model choice_set = CustomFieldChoiceSet.objects.create( @@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) # Create some tags tags = ( diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 574452a81..7ca18250c 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,11 +1,11 @@ import datetime from decimal import Decimal -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.forms import SiteImportForm from dcim.models import Manufacturer, Rack, Site @@ -28,7 +28,7 @@ class CustomFieldTest(TestCase): Site(name='Site C', slug='site-c'), ]) - cls.object_type = ContentType.objects.get_for_model(Site) + cls.object_type = ObjectType.objects.get_for_model(Site) def test_invalid_name(self): """ @@ -50,7 +50,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_TEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -75,7 +75,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_LONGTEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -99,7 +99,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -125,7 +125,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DECIMAL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -151,7 +151,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -178,7 +178,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATE, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -203,7 +203,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATETIME, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -228,7 +228,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_URL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -253,7 +253,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_JSON, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -290,7 +290,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -327,7 +327,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -350,10 +350,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -382,10 +382,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -402,13 +402,13 @@ class CustomFieldTest(TestCase): self.assertIsNone(instance.custom_field_data.get(cf.name)) def test_rename_customfield(self): - obj_type = ContentType.objects.get_for_model(Site) + obj_type = ObjectType.objects.get_for_model(Site) FIELD_DATA = 'abc' # Create a custom field cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1') cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([obj_type]) # Assign custom field data to an object site = Site.objects.create( @@ -437,7 +437,7 @@ class CustomFieldTest(TestCase): ) ) site = Site.objects.create(name='Site 1', slug='site-1') - object_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Text CustomField(name='test', type='text', required=True, default="Default text").full_clean() @@ -524,10 +524,10 @@ class CustomFieldManagerTest(TestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') custom_field.save() - custom_field.content_types.set([content_type]) + custom_field.object_types.set([object_type]) def test_get_for_model(self): self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1) @@ -538,7 +538,7 @@ class CustomFieldAPITest(APITestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Create some VLANs vlans = ( @@ -581,19 +581,19 @@ class CustomFieldAPITest(APITestCase): CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=vlans[0].pk, ), CustomField( type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, name='multiobject_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), ) for cf in custom_fields: cf.save() - cf.content_types.set([content_type]) + cf.object_types.set([object_type]) # Create some sites *after* creating the custom fields. This ensures that # default values are not set for the assigned objects. @@ -1163,7 +1163,7 @@ class CustomFieldImportTest(TestCase): ) for cf in custom_fields: cf.save() - cf.content_types.set([ContentType.objects.get_for_model(Site)]) + cf.object_types.set([ObjectType.objects.get_for_model(Site)]) def test_import(self): """ @@ -1256,11 +1256,11 @@ class CustomFieldModelTest(TestCase): def setUpTestData(cls): cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo') cf1.save() - cf1.content_types.set([ContentType.objects.get_for_model(Site)]) + cf1.object_types.set([ObjectType.objects.get_for_model(Site)]) cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar') cf2.save() - cf2.content_types.set([ContentType.objects.get_for_model(Rack)]) + cf2.object_types.set([ObjectType.objects.get_for_model(Rack)]) def test_cf_data(self): """ @@ -1299,7 +1299,7 @@ class CustomFieldModelTest(TestCase): """ cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True) cf3.save() - cf3.content_types.set([ContentType.objects.get_for_model(Site)]) + cf3.object_types.set([ObjectType.objects.get_for_model(Site)]) site = Site(name='Test Site', slug='test-site') @@ -1318,7 +1318,7 @@ class CustomFieldModelFilterTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) manufacturers = Manufacturer.objects.bulk_create(( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -1335,17 +1335,17 @@ class CustomFieldModelFilterTest(TestCase): # Integer filtering cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Decimal filtering cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Boolean filtering cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact text filtering cf = CustomField( @@ -1354,7 +1354,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose text filtering cf = CustomField( @@ -1363,12 +1363,12 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Date filtering cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact URL filtering cf = CustomField( @@ -1377,7 +1377,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose URL filtering cf = CustomField( @@ -1386,7 +1386,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Selection filtering cf = CustomField( @@ -1395,7 +1395,7 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multiselect filtering cf = CustomField( @@ -1404,25 +1404,25 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Object filtering cf = CustomField( name='cf11', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multi-object filtering cf = CustomField( name='cf12', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) Site.objects.bulk_create([ Site(name='Site 1', slug='site-1', custom_field_data={ diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index ef8aedcbd..25263fe68 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -7,6 +7,7 @@ from django.test import TestCase from circuits.models import Provider from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import Location @@ -87,11 +88,11 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): ), ) CustomField.objects.bulk_create(custom_fields) - custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site')) - custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack')) - custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) + custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack')) + custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[4].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) def test_q(self): params = {'q': 'foobar1'} @@ -101,10 +102,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Field 1', 'Custom Field 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'object_types_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_required(self): diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 9c22bf83c..5adb57fb2 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.forms import SiteForm from dcim.models import Site from extras.choices import CustomFieldTypeChoices @@ -12,66 +13,66 @@ class CustomFieldModelFormTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) choice_set = CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C')) ) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) - cf_text.content_types.set([obj_type]) + cf_text.object_types.set([object_type]) cf_longtext = CustomField.objects.create(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT) - cf_longtext.content_types.set([obj_type]) + cf_longtext.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN) - cf_boolean.content_types.set([obj_type]) + cf_boolean.object_types.set([object_type]) cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) - cf_date.content_types.set([obj_type]) + cf_date.object_types.set([object_type]) cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME) - cf_datetime.content_types.set([obj_type]) + cf_datetime.object_types.set([object_type]) cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) - cf_url.content_types.set([obj_type]) + cf_url.object_types.set([object_type]) cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON) - cf_json.content_types.set([obj_type]) + cf_json.object_types.set([object_type]) cf_select = CustomField.objects.create( name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choice_set=choice_set ) - cf_select.content_types.set([obj_type]) + cf_select.object_types.set([object_type]) cf_multiselect = CustomField.objects.create( name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choice_set=choice_set ) - cf_multiselect.content_types.set([obj_type]) + cf_multiselect.object_types.set([object_type]) cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_object.content_types.set([obj_type]) + cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_multiobject.content_types.set([obj_type]) + cf_multiobject.object_types.set([object_type]) def test_empty_values(self): """ diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index d720560e4..699388c53 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from core.models import ObjectType from dcim.models import DeviceType, Manufacturer, Site from extras.choices import * from extras.models import * @@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=( @@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) for customfield in custom_fields: customfield.save() - customfield.content_types.add(site_ct) + customfield.object_types.add(site_type) cls.form_data = { 'name': 'field_x', 'label': 'Field X', 'type': 'text', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'search_weight': 2000, 'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT, 'default': None, @@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,label,type,content_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,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/extras/views.py b/netbox/extras/views.py index 56840f7a9..49c6dcab3 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -46,9 +46,9 @@ class CustomFieldView(generic.ObjectView): def get_extra_context(self, request, instance): related_models = () - for content_type in instance.content_types.all(): + for object_type in instance.object_types.all(): related_models += ( - content_type.model_class().objects.restrict(request.user, 'view').exclude( + object_type.model_class().objects.restrict(request.user, 'view').exclude( Q(**{f'custom_field_data__{instance.name}': ''}) | Q(**{f'custom_field_data__{instance.name}': None}) ), diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 9d8286968..7df498ac6 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -5,6 +5,7 @@ from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from core.models import ObjectType from extras.models import ExportTemplate from netbox.api.serializers import BulkOperationSerializer @@ -26,9 +27,9 @@ class CustomFieldsMixin: context = super().get_serializer_context() if hasattr(self.queryset.model, 'custom_fields'): - content_type = ContentType.objects.get_for_model(self.queryset.model) + object_type = ObjectType.objects.get_for_model(self.queryset.model) context.update({ - 'custom_fields': content_type.custom_fields.all(), + 'custom_fields': object_type.custom_fields.all(), }) return context diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ebb98d15f..7f07cfbfb 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -281,7 +281,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): # Dynamically add a Filter for each CustomField applicable to the parent model custom_fields = CustomField.objects.filter( - content_types=ContentType.objects.get_for_model(self._meta.model) + object_types=ContentType.objects.get_for_model(self._meta.model) ).exclude( filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED ) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 7e1eaa80c..12de3b12b 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -88,7 +88,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): def _get_custom_fields(self, content_type): return CustomField.objects.filter( - content_types=content_type, + object_types=content_type, ui_editable=CustomFieldUIEditableChoices.YES ) diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 815f1f6fa..2f903db5d 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from core.models import ObjectType from extras.choices import * from extras.models import * from utilities.forms.fields import DynamicModelMultipleChoiceField @@ -32,16 +33,16 @@ class CustomFieldsMixin: def _get_content_type(self): """ - Return the ContentType of the form's model. + Return the ObjectType of the form's model. """ if not getattr(self, 'model', None): raise NotImplementedError(_("{class_name} must specify a model class.").format( class_name=self.__class__.__name__ )) - return ContentType.objects.get_for_model(self.model) + return ObjectType.objects.get_for_model(self.model) def _get_custom_fields(self, content_type): - return CustomField.objects.filter(content_types=content_type).exclude( + return CustomField.objects.filter(object_types=content_type).exclude( ui_editable=CustomFieldUIEditableChoices.HIDDEN ) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index cb7d2c8ba..c140462ec 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -70,8 +70,8 @@ class CoreMiddleware: return # Cleanly handle exceptions that occur from REST API requests - if is_api_request(request): - return rest_api_server_error(request) + # if is_api_request(request): + # return rest_api_server_error(request) # Ignore Http404s (defer to Django's built-in 404 handling) if isinstance(exception, Http404): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 1fb23a37c..0d7667087 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -11,6 +11,7 @@ from django.utils.module_loading import import_string import netaddr from netaddr.core import AddrFormatError +from core.models import ObjectType from extras.models import CachedValue, CustomField from netbox.registry import registry from utilities.querysets import RestrictedPrefetch @@ -134,7 +135,7 @@ class CachedValueSearchBackend(SearchBackend): # objects). This must be done before generating the final results list, which returns # a RawQuerySet. content_type_ids = set(queryset.values_list('object_type', flat=True)) - content_types = ContentType.objects.filter(pk__in=content_type_ids) + object_types = ObjectType.objects.filter(pk__in=content_type_ids) # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. @@ -153,7 +154,7 @@ class CachedValueSearchBackend(SearchBackend): # Iterate through each ContentType represented in the search results and prefetch any # related objects necessary to render the prescribed display attributes (display_attrs). - for ct in content_types: + for ct in object_types: model = ct.model_class() indexer = registry['search'].get(content_type_identifier(ct)) if not (display_attrs := getattr(indexer, 'display_attrs', None)): @@ -182,7 +183,7 @@ class CachedValueSearchBackend(SearchBackend): return ret def cache(self, instances, indexer=None, remove_existing=True): - content_type = None + object_type = None custom_fields = None # Convert a single instance to an iterable @@ -204,8 +205,8 @@ class CachedValueSearchBackend(SearchBackend): break # Prefetch any associated custom fields - content_type = ContentType.objects.get_for_model(indexer.model) - custom_fields = CustomField.objects.filter(content_types=content_type).exclude(search_weight=0) + object_type = ObjectType.objects.get_for_model(indexer.model) + custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0) # Wipe out any previously cached values for the object if remove_existing: @@ -215,7 +216,7 @@ class CachedValueSearchBackend(SearchBackend): for field in indexer.to_cache(instance, custom_fields=custom_fields): buffer.append( CachedValue( - object_type=content_type, + object_type=object_type, object_id=instance.pk, field=field.name, type=field.type, diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 495e56991..597d73a16 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -3,7 +3,6 @@ from copy import deepcopy import django_tables2 as tables from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField from django.urls import reverse @@ -12,6 +11,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django_tables2.data import TableQuerysetData +from core.models import ObjectType from extras.choices import * from extras.models import CustomField, CustomLink from netbox.registry import registry @@ -201,14 +201,14 @@ class NetBoxTable(BaseTable): ]) # Add custom field & custom link columns - content_type = ContentType.objects.get_for_model(self._meta.model) + object_type = ObjectType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter( - content_types=content_type + object_types=object_type ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN) extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) - custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) + custom_links = CustomLink.objects.filter(content_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links ]) diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index aa2093a9a..f16c2fbbd 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -10,6 +10,7 @@ from django.test import Client, TestCase as _TestCase from netaddr import IPNetwork from taggit.managers import TaggableManager +from core.models import ObjectType from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct from utilities.utils import content_type_identifier @@ -112,7 +113,7 @@ class ModelTestCase(TestCase): # Handle ManyToManyFields if value and type(field) in (ManyToManyField, TaggableManager): - if field.related_model is ContentType and api: + if field.related_model in (ContentType, ObjectType) and api: model_dict[key] = sorted([content_type_identifier(ct) for ct in value]) else: model_dict[key] = sorted([obj.pk for obj in value]) @@ -120,8 +121,8 @@ class ModelTestCase(TestCase): elif api: # Replace ContentType numeric IDs with . - if type(getattr(instance, key)) is ContentType: - ct = ContentType.objects.get(pk=value) + if type(getattr(instance, key)) in (ContentType, ObjectType): + ct = ObjectType.objects.get(pk=value) model_dict[key] = content_type_identifier(ct) # Convert IPNetwork instances to strings diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index 1cc3487b1..81be70a34 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -1,10 +1,8 @@ -import urllib.parse - -from django.contrib.contenttypes.models import ContentType from django.test import Client, TestCase, override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.models import Region, Site from extras.choices import CustomFieldTypeChoices from extras.models import CustomField @@ -240,10 +238,10 @@ class APIDocsTestCase(TestCase): self.client = Client() # Populate a CustomField to activate CustomFieldSerializer - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='test') self.cf_text.save() - self.cf_text.content_types.set([content_type]) + self.cf_text.object_types.set([object_type]) self.cf_text.save() def test_api_docs(self):