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.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from circuits.models import * from circuits.models import *
from core.models import ObjectType
from dcim.choices import * from dcim.choices import *
from dcim.models import * from dcim.models import *
from extras.models import CustomField 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 # Create a CustomField with a default value & assign it to all component models
cf1 = CustomField.objects.create(name='cf1', default='foo') cf1 = CustomField.objects.create(name='cf1', default='foo')
cf1.content_types.set( cf1.object_types.set(
ContentType.objects.filter(app_label='dcim', model__in=[ ObjectType.objects.filter(app_label='dcim', model__in=[
'consoleport', 'consoleport',
'consoleserverport', 'consoleserverport',
'powerport', 'powerport',

View File

@ -26,7 +26,7 @@ class CustomFieldDefaultValues:
# Retrieve the CustomFields for the parent model # Retrieve the CustomFields for the parent model
content_type = ContentType.objects.get_for_model(self.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 # Populate the default value for each CustomField
value = {} value = {}
@ -48,7 +48,7 @@ class CustomFieldsDataField(Field):
""" """
if not hasattr(self, '_custom_fields'): if not hasattr(self, '_custom_fields'):
content_type = ContentType.objects.get_for_model(self.parent.Meta.model) 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 return self._custom_fields
def to_representation(self, obj): def to_representation(self, obj):

View File

@ -117,7 +117,7 @@ class WebhookSerializer(NetBoxModelSerializer):
class CustomFieldSerializer(ValidatedModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
content_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
many=True many=True
) )
@ -139,7 +139,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ 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', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
'created', 'last_updated', 'created', 'last_updated',

View File

@ -124,10 +124,12 @@ class CustomFieldFilterSet(BaseFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices choices=CustomFieldTypeChoices
) )
content_type_id = MultiValueNumberFilter( object_types_id = MultiValueNumberFilter(
field_name='content_types__id' field_name='object_types__id'
)
object_types = ContentTypeFilter(
field_name='object_types'
) )
content_types = ContentTypeFilter()
choice_set_id = django_filters.ModelMultipleChoiceFilter( choice_set_id = django_filters.ModelMultipleChoiceFilter(
queryset=CustomFieldChoiceSet.objects.all() queryset=CustomFieldChoiceSet.objects.all()
) )
@ -140,7 +142,7 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ 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', 'ui_editable', 'weight', 'is_cloneable', 'description',
] ]

View File

@ -30,8 +30,8 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm): class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
class Meta: class Meta:
model = CustomField model = CustomField
fields = ( 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', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
) )

View File

@ -38,11 +38,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ( (_('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', 'is_cloneable',
)), )),
) )
content_type_id = ContentTypeMultipleChoiceField( object_types_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
required=False, required=False,
label=_('Object type') label=_('Object type')

View File

@ -2,7 +2,6 @@ import json
import re import re
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -39,8 +38,8 @@ __all__ = (
class CustomFieldForm(forms.ModelForm): class CustomFieldForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_fields') queryset=ObjectType.objects.with_feature('custom_fields')
) )
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm):
fieldsets = ( fieldsets = (
(_('Custom Field'), ( (_('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')), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')), (_('Values'), ('default', 'choice_set')),

View File

@ -39,7 +39,7 @@ class CustomFieldType(ObjectType):
class Meta: class Meta:
model = models.CustomField model = models.CustomField
exclude = ('content_types', ) exclude = ('object_types', 'object_type')
filterset_class = filtersets.CustomFieldFilterSet 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. Return all CustomFields assigned to the given model.
""" """
content_type = ObjectType.objects.get_for_model(model._meta.concrete_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): def get_defaults_for_model(self, model):
""" """
@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='custom_fields', related_name='custom_fields',
help_text=_('The object(s) to which this field applies.') 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') help_text=_('The type of data this custom field holds')
) )
object_type = models.ForeignKey( object_type = models.ForeignKey(
to='contenttypes.ContentType', to='core.ObjectType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
null=True, null=True,
@ -284,7 +284,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
""" """
Called when a CustomField has been renamed. Updates all assigned object data. 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() model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False} params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params) 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. 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) post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, 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_added_obj_types, sender=CustomField.object_types.through)
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_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 rest_framework import status
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'name', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'url']
create_data = [ create_data = [
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf4', 'name': 'cf4',
'type': 'date', 'type': 'date',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf5', 'name': 'cf5',
'type': 'url', 'type': 'url',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf6', 'name': 'cf6',
'type': 'text', 'type': 'text',
}, },
@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
'description': 'New description', 'description': 'New description',
} }
update_data = { update_data = {
'content_types': ['dcim.device'], 'object_types': ['dcim.device'],
'name': 'New_Name', 'name': 'New_Name',
'description': 'New description', 'description': 'New description',
} }
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_ct = ObjectType.objects.get_for_model(Site)
custom_fields = ( custom_fields = (
CustomField( CustomField(
@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
) )
CustomField.objects.bulk_create(custom_fields) CustomField.objects.bulk_create(custom_fields)
for cf in custom_fields: for cf in custom_fields:
cf.content_types.add(site_ct) cf.object_types.add(site_ct)
class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):

View File

@ -3,6 +3,7 @@ from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from core.models import ObjectType
from dcim.choices import SiteStatusChoices from dcim.choices import SiteStatusChoices
from dcim.models import Site from dcim.models import Site
from extras.choices import * from extras.choices import *
@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase):
) )
# Create a custom field on the Site model # 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( cf = CustomField(
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
name='cf1', name='cf1',
required=False required=False
) )
cf.save() cf.save()
cf.content_types.set([ct]) cf.object_types.set([site_type])
# Create a select custom field on the Site model # Create a select custom field on the Site model
cf_select = CustomField( cf_select = CustomField(
@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase):
choice_set=choice_set choice_set=choice_set
) )
cf_select.save() cf_select.save()
cf_select.content_types.set([ct]) cf_select.object_types.set([site_type])
def test_create_object(self): def test_create_object(self):
tags = create_tags('Tag 1', 'Tag 2') tags = create_tags('Tag 1', 'Tag 2')
@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase):
def setUpTestData(cls): def setUpTestData(cls):
# Create a custom field on the Site model # 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( cf = CustomField(
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
name='cf1', name='cf1',
required=False required=False
) )
cf.save() cf.save()
cf.content_types.set([ct]) cf.object_types.set([site_type])
# Create a select custom field on the Site model # Create a select custom field on the Site model
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase):
choice_set=choice_set choice_set=choice_set
) )
cf_select.save() cf_select.save()
cf_select.content_types.set([ct]) cf_select.object_types.set([site_type])
# Create some tags # Create some tags
tags = ( tags = (

View File

@ -1,11 +1,11 @@
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from core.models import ObjectType
from dcim.filtersets import SiteFilterSet from dcim.filtersets import SiteFilterSet
from dcim.forms import SiteImportForm from dcim.forms import SiteImportForm
from dcim.models import Manufacturer, Rack, Site from dcim.models import Manufacturer, Rack, Site
@ -28,7 +28,7 @@ class CustomFieldTest(TestCase):
Site(name='Site C', slug='site-c'), 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): def test_invalid_name(self):
""" """
@ -50,7 +50,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -75,7 +75,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_LONGTEXT, type=CustomFieldTypeChoices.TYPE_LONGTEXT,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -99,7 +99,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_INTEGER, type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -125,7 +125,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DECIMAL, type=CustomFieldTypeChoices.TYPE_DECIMAL,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -151,7 +151,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_INTEGER, type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -178,7 +178,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DATE, type=CustomFieldTypeChoices.TYPE_DATE,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -203,7 +203,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DATETIME, type=CustomFieldTypeChoices.TYPE_DATETIME,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -228,7 +228,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_URL, type=CustomFieldTypeChoices.TYPE_URL,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -253,7 +253,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_JSON, type=CustomFieldTypeChoices.TYPE_JSON,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -290,7 +290,7 @@ class CustomFieldTest(TestCase):
required=False, required=False,
choice_set=choice_set choice_set=choice_set
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -327,7 +327,7 @@ class CustomFieldTest(TestCase):
required=False, required=False,
choice_set=choice_set choice_set=choice_set
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -350,10 +350,10 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(VLAN), object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -382,10 +382,10 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(VLAN), object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -402,13 +402,13 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_rename_customfield(self): def test_rename_customfield(self):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ObjectType.objects.get_for_model(Site)
FIELD_DATA = 'abc' FIELD_DATA = 'abc'
# Create a custom field # Create a custom field
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1') cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([obj_type])
# Assign custom field data to an object # Assign custom field data to an object
site = Site.objects.create( site = Site.objects.create(
@ -437,7 +437,7 @@ class CustomFieldTest(TestCase):
) )
) )
site = Site.objects.create(name='Site 1', slug='site-1') 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 # Text
CustomField(name='test', type='text', required=True, default="Default text").full_clean() CustomField(name='test', type='text', required=True, default="Default text").full_clean()
@ -524,10 +524,10 @@ class CustomFieldManagerTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): 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 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
custom_field.save() custom_field.save()
custom_field.content_types.set([content_type]) custom_field.object_types.set([object_type])
def test_get_for_model(self): def test_get_for_model(self):
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1) self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
@ -538,7 +538,7 @@ class CustomFieldAPITest(APITestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
# Create some VLANs # Create some VLANs
vlans = ( vlans = (
@ -581,19 +581,19 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
name='object_field', name='object_field',
object_type=ContentType.objects.get_for_model(VLAN), object_type=ObjectType.objects.get_for_model(VLAN),
default=vlans[0].pk, default=vlans[0].pk,
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
name='multiobject_field', name='multiobject_field',
object_type=ContentType.objects.get_for_model(VLAN), object_type=ObjectType.objects.get_for_model(VLAN),
default=[vlans[0].pk, vlans[1].pk], default=[vlans[0].pk, vlans[1].pk],
), ),
) )
for cf in custom_fields: for cf in custom_fields:
cf.save() 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 # Create some sites *after* creating the custom fields. This ensures that
# default values are not set for the assigned objects. # default values are not set for the assigned objects.
@ -1163,7 +1163,7 @@ class CustomFieldImportTest(TestCase):
) )
for cf in custom_fields: for cf in custom_fields:
cf.save() 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): def test_import(self):
""" """
@ -1256,11 +1256,11 @@ class CustomFieldModelTest(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo') cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
cf1.save() 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 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
cf2.save() 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): def test_cf_data(self):
""" """
@ -1299,7 +1299,7 @@ class CustomFieldModelTest(TestCase):
""" """
cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True) cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
cf3.save() 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') site = Site(name='Test Site', slug='test-site')
@ -1318,7 +1318,7 @@ class CustomFieldModelFilterTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
manufacturers = Manufacturer.objects.bulk_create(( manufacturers = Manufacturer.objects.bulk_create((
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@ -1335,17 +1335,17 @@ class CustomFieldModelFilterTest(TestCase):
# Integer filtering # Integer filtering
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Decimal filtering # Decimal filtering
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Boolean filtering # Boolean filtering
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Exact text filtering # Exact text filtering
cf = CustomField( cf = CustomField(
@ -1354,7 +1354,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Loose text filtering # Loose text filtering
cf = CustomField( cf = CustomField(
@ -1363,12 +1363,12 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Date filtering # Date filtering
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE) cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Exact URL filtering # Exact URL filtering
cf = CustomField( cf = CustomField(
@ -1377,7 +1377,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Loose URL filtering # Loose URL filtering
cf = CustomField( cf = CustomField(
@ -1386,7 +1386,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Selection filtering # Selection filtering
cf = CustomField( cf = CustomField(
@ -1395,7 +1395,7 @@ class CustomFieldModelFilterTest(TestCase):
choice_set=choice_set choice_set=choice_set
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Multiselect filtering # Multiselect filtering
cf = CustomField( cf = CustomField(
@ -1404,25 +1404,25 @@ class CustomFieldModelFilterTest(TestCase):
choice_set=choice_set choice_set=choice_set
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Object filtering # Object filtering
cf = CustomField( cf = CustomField(
name='cf11', name='cf11',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(Manufacturer) object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Multi-object filtering # Multi-object filtering
cf = CustomField( cf = CustomField(
name='cf12', name='cf12',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(Manufacturer) object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
Site.objects.bulk_create([ Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', custom_field_data={ 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 circuits.models import Provider
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import ObjectType
from dcim.filtersets import SiteFilterSet from dcim.filtersets import SiteFilterSet
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from dcim.models import Location from dcim.models import Location
@ -87,11 +88,11 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
), ),
) )
CustomField.objects.bulk_create(custom_fields) CustomField.objects.bulk_create(custom_fields)
custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site')) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack')) custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))
custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
custom_fields[4].content_types.add(ContentType.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): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -101,10 +102,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'name': ['Custom Field 1', 'Custom Field 2']} params = {'name': ['Custom Field 1', 'Custom Field 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_types(self):
params = {'content_types': 'dcim.site'} params = {'object_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_required(self): def test_required(self):

View File

@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from core.models import ObjectType
from dcim.forms import SiteForm from dcim.forms import SiteForm
from dcim.models import Site from dcim.models import Site
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
@ -12,66 +13,66 @@ class CustomFieldModelFormTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C')) extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
) )
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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( cf_select = CustomField.objects.create(
name='select', name='select',
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
choice_set=choice_set choice_set=choice_set
) )
cf_select.content_types.set([obj_type]) cf_select.object_types.set([object_type])
cf_multiselect = CustomField.objects.create( cf_multiselect = CustomField.objects.create(
name='multiselect', name='multiselect',
type=CustomFieldTypeChoices.TYPE_MULTISELECT, type=CustomFieldTypeChoices.TYPE_MULTISELECT,
choice_set=choice_set choice_set=choice_set
) )
cf_multiselect.content_types.set([obj_type]) cf_multiselect.object_types.set([object_type])
cf_object = CustomField.objects.create( cf_object = CustomField.objects.create(
name='object', name='object',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(Site) 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( cf_multiobject = CustomField.objects.create(
name='multiobject', name='multiobject',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(Site) 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): 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.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from core.models import ObjectType
from dcim.models import DeviceType, Manufacturer, Site from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
CustomFieldChoiceSet.objects.create( CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=( extra_choices=(
@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
for customfield in custom_fields: for customfield in custom_fields:
customfield.save() customfield.save()
customfield.content_types.add(site_ct) customfield.object_types.add(site_type)
cls.form_data = { cls.form_data = {
'name': 'field_x', 'name': 'field_x',
'label': 'Field X', 'label': 'Field X',
'type': 'text', 'type': 'text',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'search_weight': 2000, 'search_weight': 2000,
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT, 'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
'default': None, 'default': None,
@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( 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', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes',
'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes',
'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',

View File

@ -46,9 +46,9 @@ class CustomFieldView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = () related_models = ()
for content_type in instance.content_types.all(): for object_type in instance.object_types.all():
related_models += ( 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}': ''}) |
Q(**{f'custom_field_data__{instance.name}': None}) 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 import status
from rest_framework.response import Response from rest_framework.response import Response
from core.models import ObjectType
from extras.models import ExportTemplate from extras.models import ExportTemplate
from netbox.api.serializers import BulkOperationSerializer from netbox.api.serializers import BulkOperationSerializer
@ -26,9 +27,9 @@ class CustomFieldsMixin:
context = super().get_serializer_context() context = super().get_serializer_context()
if hasattr(self.queryset.model, 'custom_fields'): 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({ context.update({
'custom_fields': content_type.custom_fields.all(), 'custom_fields': object_type.custom_fields.all(),
}) })
return context return context

View File

@ -281,7 +281,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
# Dynamically add a Filter for each CustomField applicable to the parent model # Dynamically add a Filter for each CustomField applicable to the parent model
custom_fields = CustomField.objects.filter( 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( ).exclude(
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
) )

View File

@ -88,7 +88,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter( return CustomField.objects.filter(
content_types=content_type, object_types=content_type,
ui_editable=CustomFieldUIEditableChoices.YES ui_editable=CustomFieldUIEditableChoices.YES
) )

View File

@ -2,6 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.fields import DynamicModelMultipleChoiceField
@ -32,16 +33,16 @@ class CustomFieldsMixin:
def _get_content_type(self): 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): if not getattr(self, 'model', None):
raise NotImplementedError(_("{class_name} must specify a model class.").format( raise NotImplementedError(_("{class_name} must specify a model class.").format(
class_name=self.__class__.__name__ 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): 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 ui_editable=CustomFieldUIEditableChoices.HIDDEN
) )

View File

@ -70,8 +70,8 @@ class CoreMiddleware:
return return
# Cleanly handle exceptions that occur from REST API requests # Cleanly handle exceptions that occur from REST API requests
if is_api_request(request): # if is_api_request(request):
return rest_api_server_error(request) # return rest_api_server_error(request)
# Ignore Http404s (defer to Django's built-in 404 handling) # Ignore Http404s (defer to Django's built-in 404 handling)
if isinstance(exception, Http404): if isinstance(exception, Http404):

View File

@ -11,6 +11,7 @@ from django.utils.module_loading import import_string
import netaddr import netaddr
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from core.models import ObjectType
from extras.models import CachedValue, CustomField from extras.models import CachedValue, CustomField
from netbox.registry import registry from netbox.registry import registry
from utilities.querysets import RestrictedPrefetch 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 # objects). This must be done before generating the final results list, which returns
# a RawQuerySet. # a RawQuerySet.
content_type_ids = set(queryset.values_list('object_type', flat=True)) 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 # Construct a Prefetch to pre-fetch only those related objects for which the
# user has permission to view. # user has permission to view.
@ -153,7 +154,7 @@ class CachedValueSearchBackend(SearchBackend):
# Iterate through each ContentType represented in the search results and prefetch any # Iterate through each ContentType represented in the search results and prefetch any
# related objects necessary to render the prescribed display attributes (display_attrs). # 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() model = ct.model_class()
indexer = registry['search'].get(content_type_identifier(ct)) indexer = registry['search'].get(content_type_identifier(ct))
if not (display_attrs := getattr(indexer, 'display_attrs', None)): if not (display_attrs := getattr(indexer, 'display_attrs', None)):
@ -182,7 +183,7 @@ class CachedValueSearchBackend(SearchBackend):
return ret return ret
def cache(self, instances, indexer=None, remove_existing=True): def cache(self, instances, indexer=None, remove_existing=True):
content_type = None object_type = None
custom_fields = None custom_fields = None
# Convert a single instance to an iterable # Convert a single instance to an iterable
@ -204,8 +205,8 @@ class CachedValueSearchBackend(SearchBackend):
break break
# Prefetch any associated custom fields # Prefetch any associated custom fields
content_type = ContentType.objects.get_for_model(indexer.model) object_type = ObjectType.objects.get_for_model(indexer.model)
custom_fields = CustomField.objects.filter(content_types=content_type).exclude(search_weight=0) custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0)
# Wipe out any previously cached values for the object # Wipe out any previously cached values for the object
if remove_existing: if remove_existing:
@ -215,7 +216,7 @@ class CachedValueSearchBackend(SearchBackend):
for field in indexer.to_cache(instance, custom_fields=custom_fields): for field in indexer.to_cache(instance, custom_fields=custom_fields):
buffer.append( buffer.append(
CachedValue( CachedValue(
object_type=content_type, object_type=object_type,
object_id=instance.pk, object_id=instance.pk,
field=field.name, field=field.name,
type=field.type, type=field.type,

View File

@ -3,7 +3,6 @@ from copy import deepcopy
import django_tables2 as tables import django_tables2 as tables
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField from django.db.models.fields.related import RelatedField
from django.urls import reverse 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.utils.translation import gettext_lazy as _
from django_tables2.data import TableQuerysetData from django_tables2.data import TableQuerysetData
from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.models import CustomField, CustomLink from extras.models import CustomField, CustomLink
from netbox.registry import registry from netbox.registry import registry
@ -201,14 +201,14 @@ class NetBoxTable(BaseTable):
]) ])
# Add custom field & custom link columns # 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( custom_fields = CustomField.objects.filter(
content_types=content_type object_types=object_type
).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN) ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
extra_columns.extend([ extra_columns.extend([
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields (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([ extra_columns.extend([
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links (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 netaddr import IPNetwork
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from core.models import ObjectType
from users.models import ObjectPermission from users.models import ObjectPermission
from utilities.permissions import resolve_permission_ct from utilities.permissions import resolve_permission_ct
from utilities.utils import content_type_identifier from utilities.utils import content_type_identifier
@ -112,7 +113,7 @@ class ModelTestCase(TestCase):
# Handle ManyToManyFields # Handle ManyToManyFields
if value and type(field) in (ManyToManyField, TaggableManager): 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]) model_dict[key] = sorted([content_type_identifier(ct) for ct in value])
else: else:
model_dict[key] = sorted([obj.pk for obj in value]) model_dict[key] = sorted([obj.pk for obj in value])
@ -120,8 +121,8 @@ class ModelTestCase(TestCase):
elif api: elif api:
# Replace ContentType numeric IDs with <app_label>.<model> # Replace ContentType numeric IDs with <app_label>.<model>
if type(getattr(instance, key)) is ContentType: if type(getattr(instance, key)) in (ContentType, ObjectType):
ct = ContentType.objects.get(pk=value) ct = ObjectType.objects.get(pk=value)
model_dict[key] = content_type_identifier(ct) model_dict[key] = content_type_identifier(ct)
# Convert IPNetwork instances to strings # 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.test import Client, TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from core.models import ObjectType
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField from extras.models import CustomField
@ -240,10 +238,10 @@ class APIDocsTestCase(TestCase):
self.client = Client() self.client = Client()
# Populate a CustomField to activate CustomFieldSerializer # 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 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='test')
self.cf_text.save() self.cf_text.save()
self.cf_text.content_types.set([content_type]) self.cf_text.object_types.set([object_type])
self.cf_text.save() self.cf_text.save()
def test_api_docs(self): def test_api_docs(self):