Просмотр исходного кода

Rename CustomField.content_types to object_types & use ObjectType proxy

Jeremy Stretch 2 лет назад
Родитель
Сommit
aeeec284a5

+ 3 - 3
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',

+ 2 - 2
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):

+ 2 - 2
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',

+ 6 - 4
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',
         ]
 

+ 3 - 3
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',
         )

+ 2 - 2
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')

+ 3 - 4
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')),

+ 1 - 1
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
 
 

+ 28 - 0
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'),
+        ),
+    ]

+ 5 - 5
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)

+ 3 - 3
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)
 
 
 #

+ 7 - 7
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):

+ 7 - 6
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 = (

+ 45 - 45
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={

+ 9 - 8
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):

+ 15 - 14
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):
         """

+ 5 - 4
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',

+ 2 - 2
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})
                 ),

+ 3 - 2
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

+ 1 - 1
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
         )

+ 1 - 1
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
         )
 

+ 4 - 3
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
         )
 

+ 2 - 2
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):

+ 7 - 6
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,

+ 4 - 4
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
         ])

+ 4 - 3
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 <app_label>.<model>
-                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

+ 3 - 5
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):