Rename CustomField.content_types to object_types & use ObjectType proxy

This commit is contained in:
Jeremy Stretch 2024-03-01 14:36:35 -05:00
parent 0df68bf291
commit aeeec284a5
27 changed files with 177 additions and 142 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ class CustomFieldType(ObjectType):
class Meta:
model = models.CustomField
exclude = ('content_types', )
exclude = ('object_types', 'object_type')
filterset_class = filtersets.CustomFieldFilterSet

View File

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

View File

@ -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)

View File

@ -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)
#

View File

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

View File

@ -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 = (

View File

@ -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={

View File

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

View File

@ -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):
"""

View File

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

View File

@ -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})
),

View File

@ -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

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

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

View File

@ -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,

View File

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

View File

@ -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

View File

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