Răsfoiți Sursa

Merge pull request #3932 from netbox-community/3892-contenttype-filtering

Closes #3892: Robust ContentType filtering
Jeremy Stretch 6 ani în urmă
părinte
comite
4073dedff8

+ 2 - 2
netbox/dcim/api/serializers.py

@@ -609,10 +609,10 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
 
 
 class CableSerializer(ValidatedModelSerializer):
 class CableSerializer(ValidatedModelSerializer):
     termination_a_type = ContentTypeField(
     termination_a_type = ContentTypeField(
-        queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
     )
     )
     termination_b_type = ContentTypeField(
     termination_b_type = ContentTypeField(
-        queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
     )
     )
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_a = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)
     termination_b = serializers.SerializerMethodField(read_only=True)

+ 17 - 4
netbox/dcim/constants.py

@@ -1,3 +1,5 @@
+from django.db.models import Q
+
 from .choices import InterfaceTypeChoices
 from .choices import InterfaceTypeChoices
 
 
 
 
@@ -43,10 +45,21 @@ CONNECTION_STATUS_CHOICES = [
 ]
 ]
 
 
 # Cable endpoint types
 # Cable endpoint types
-CABLE_TERMINATION_TYPES = [
-    'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
-    'circuittermination', 'powerfeed',
-]
+CABLE_TERMINATION_MODELS = Q(
+    Q(app_label='circuits', model__in=(
+        'circuittermination',
+    )) |
+    Q(app_label='dcim', model__in=(
+        'consoleport',
+        'consoleserverport',
+        'frontport',
+        'interface',
+        'powerfeed',
+        'poweroutlet',
+        'powerport',
+        'rearport',
+    ))
+)
 
 
 COMPATIBLE_TERMINATION_TYPES = {
 COMPATIBLE_TERMINATION_TYPES = {
     'consoleport': ['consoleserverport', 'frontport', 'rearport'],
     'consoleport': ['consoleserverport', 'frontport', 'rearport'],

+ 2 - 6
netbox/dcim/forms.py

@@ -3374,9 +3374,7 @@ class CableCSVForm(forms.ModelForm):
     )
     )
     side_a_type = forms.ModelChoiceField(
     side_a_type = forms.ModelChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
-        limit_choices_to={
-            'model__in': CABLE_TERMINATION_TYPES,
-        },
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         to_field_name='model',
         to_field_name='model',
         help_text='Side A type'
         help_text='Side A type'
     )
     )
@@ -3395,9 +3393,7 @@ class CableCSVForm(forms.ModelForm):
     )
     )
     side_b_type = forms.ModelChoiceField(
     side_b_type = forms.ModelChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
-        limit_choices_to={
-            'model__in': CABLE_TERMINATION_TYPES,
-        },
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         to_field_name='model',
         to_field_name='model',
         help_text='Side B type'
         help_text='Side B type'
     )
     )

+ 24 - 0
netbox/dcim/migrations/0090_cable_termination_models.py

@@ -0,0 +1,24 @@
+# Generated by Django 2.2.8 on 2020-01-15 20:51
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0089_deterministic_ordering'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='cable',
+            name='termination_a_type',
+            field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='cable',
+            name='termination_b_type',
+            field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+    ]

+ 2 - 2
netbox/dcim/models/__init__.py

@@ -1939,7 +1939,7 @@ class Cable(ChangeLoggedModel):
     """
     """
     termination_a_type = models.ForeignKey(
     termination_a_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
-        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='+'
         related_name='+'
     )
     )
@@ -1950,7 +1950,7 @@ class Cable(ChangeLoggedModel):
     )
     )
     termination_b_type = models.ForeignKey(
     termination_b_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
-        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='+'
         related_name='+'
     )
     )

+ 1 - 1
netbox/dcim/tests/test_api.py

@@ -30,7 +30,7 @@ class ChoicesTest(APITestCase):
         # Cable
         # Cable
         self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict())
         self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict())
         self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict())
         self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict())
-        content_types = ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS)
         cable_termination_choices = {
         cable_termination_choices = {
             "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
             "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
         }
         }

+ 1 - 2
netbox/extras/api/serializers.py

@@ -20,7 +20,6 @@ from utilities.api import (
     ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
     ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
     ValidatedModelSerializer,
     ValidatedModelSerializer,
 )
 )
-from utilities.utils import model_names_to_filter_dict
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 
@@ -30,7 +29,7 @@ from .nested_serializers import *
 
 
 class GraphSerializer(ValidatedModelSerializer):
 class GraphSerializer(ValidatedModelSerializer):
     type = ContentTypeField(
     type = ContentTypeField(
-        queryset=ContentType.objects.filter(**model_names_to_filter_dict(GRAPH_MODELS)),
+        queryset=ContentType.objects.filter(GRAPH_MODELS),
     )
     )
 
 
     class Meta:
     class Meta:

+ 162 - 107
netbox/extras/constants.py

@@ -1,85 +1,128 @@
+from django.db.models import Q
+
+
 # Models which support custom fields
 # Models which support custom fields
-CUSTOMFIELD_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.site',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+CUSTOMFIELD_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'device',
+        'devicetype',
+        'powerfeed',
+        'rack',
+        'site',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
+)
 
 
 # Custom links
 # Custom links
-CUSTOMLINK_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.powerpanel',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.site',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+CUSTOMLINK_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'device',
+        'devicetype',
+        'powerpanel',
+        'powerfeed',
+        'rack',
+        'site',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
+)
 
 
 # Models which can have Graphs associated with them
 # Models which can have Graphs associated with them
-GRAPH_MODELS = (
-    'circuits.provider',
-    'dcim.device',
-    'dcim.interface',
-    'dcim.site',
+GRAPH_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'device',
+        'interface',
+        'site',
+    ])
 )
 )
 
 
 # Models which support export templates
 # Models which support export templates
-EXPORTTEMPLATE_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.consoleport',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.interface',
-    'dcim.inventoryitem',
-    'dcim.manufacturer',
-    'dcim.powerpanel',
-    'dcim.powerport',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.rackgroup',
-    'dcim.region',
-    'dcim.site',
-    'dcim.virtualchassis',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+EXPORTTEMPLATE_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'consoleport',
+        'device',
+        'devicetype',
+        'interface',
+        'inventoryitem',
+        'manufacturer',
+        'powerpanel',
+        'powerport',
+        'powerfeed',
+        'rack',
+        'rackgroup',
+        'region',
+        'site',
+        'virtualchassis',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
+)
 
 
 # Report logging levels
 # Report logging levels
 LOG_DEFAULT = 0
 LOG_DEFAULT = 0
@@ -96,36 +139,48 @@ LOG_LEVEL_CODES = {
 }
 }
 
 
 # Models which support registered webhooks
 # Models which support registered webhooks
-WEBHOOK_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.consoleport',
-    'dcim.consoleserverport',
-    'dcim.device',
-    'dcim.devicebay',
-    'dcim.devicetype',
-    'dcim.interface',
-    'dcim.inventoryitem',
-    'dcim.frontport',
-    'dcim.manufacturer',
-    'dcim.poweroutlet',
-    'dcim.powerpanel',
-    'dcim.powerport',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.rearport',
-    'dcim.region',
-    'dcim.site',
-    'dcim.virtualchassis',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+WEBHOOK_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'consoleport',
+        'consoleserverport',
+        'device',
+        'devicebay',
+        'devicetype',
+        'frontport',
+        'interface',
+        'inventoryitem',
+        'manufacturer',
+        'poweroutlet',
+        'powerpanel',
+        'powerport',
+        'powerfeed',
+        'rack',
+        'rearport',
+        'region',
+        'site',
+        'virtualchassis',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
+)

+ 4 - 4
netbox/extras/migrations/0022_custom_links.py

@@ -22,7 +22,7 @@ class Migration(migrations.Migration):
                 ('group_name', models.CharField(blank=True, max_length=50)),
                 ('group_name', models.CharField(blank=True, max_length=50)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('new_window', models.BooleanField()),
                 ('new_window', models.BooleanField()),
-                ('content_type', models.ForeignKey(limit_choices_to=extras.models.get_custom_link_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
             ],
             ],
             options={
             options={
                 'ordering': ['group_name', 'weight', 'name'],
                 'ordering': ['group_name', 'weight', 'name'],
@@ -33,16 +33,16 @@ class Migration(migrations.Migration):
         migrations.AlterField(
         migrations.AlterField(
             model_name='customfield',
             model_name='customfield',
             name='obj_type',
             name='obj_type',
-            field=models.ManyToManyField(limit_choices_to=extras.models.get_custom_field_models, related_name='custom_fields', to='contenttypes.ContentType'),
+            field=models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType'),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='exporttemplate',
             model_name='exporttemplate',
             name='content_type',
             name='content_type',
-            field=models.ForeignKey(limit_choices_to=extras.models.get_export_template_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='webhook',
             model_name='webhook',
             name='obj_type',
             name='obj_type',
-            field=models.ManyToManyField(limit_choices_to=extras.models.get_webhook_models, related_name='webhooks', to='contenttypes.ContentType'),
+            field=models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType'),
         ),
         ),
     ]
     ]

+ 4 - 4
netbox/extras/migrations/0022_custom_links_squashed_0034_configcontext_tags.py

@@ -106,7 +106,7 @@ class Migration(migrations.Migration):
                 ('group_name', models.CharField(blank=True, max_length=50)),
                 ('group_name', models.CharField(blank=True, max_length=50)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('new_window', models.BooleanField()),
                 ('new_window', models.BooleanField()),
-                ('content_type', models.ForeignKey(limit_choices_to=extras.models.get_custom_link_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
             ],
             ],
             options={
             options={
                 'ordering': ['group_name', 'weight', 'name'],
                 'ordering': ['group_name', 'weight', 'name'],
@@ -115,17 +115,17 @@ class Migration(migrations.Migration):
         migrations.AlterField(
         migrations.AlterField(
             model_name='customfield',
             model_name='customfield',
             name='obj_type',
             name='obj_type',
-            field=models.ManyToManyField(limit_choices_to=extras.models.get_custom_field_models, related_name='custom_fields', to='contenttypes.ContentType'),
+            field=models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType'),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='exporttemplate',
             model_name='exporttemplate',
             name='content_type',
             name='content_type',
-            field=models.ForeignKey(limit_choices_to=extras.models.get_export_template_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='webhook',
             model_name='webhook',
             name='obj_type',
             name='obj_type',
-            field=models.ManyToManyField(limit_choices_to=extras.models.get_webhook_models, related_name='webhooks', to='contenttypes.ContentType'),
+            field=models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType'),
         ),
         ),
         migrations.RunSQL(
         migrations.RunSQL(
             sql="SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)",
             sql="SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)",

+ 39 - 0
netbox/extras/migrations/0036_contenttype_filters_to_q_objects.py

@@ -0,0 +1,39 @@
+# Generated by Django 2.2.8 on 2020-01-15 21:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0035_deterministic_ordering'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='customfield',
+            name='obj_type',
+            field=models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'provider'])), models.Q(('app_label', 'dcim'), ('model__in', ['device', 'devicetype', 'powerfeed', 'rack', 'site'])), models.Q(('app_label', 'ipam'), ('model__in', ['aggregate', 'ipaddress', 'prefix', 'service', 'vlan', 'vrf'])), models.Q(('app_label', 'secrets'), ('model__in', ['secret'])), models.Q(('app_label', 'tenancy'), ('model__in', ['tenant'])), models.Q(('app_label', 'virtualization'), ('model__in', ['cluster', 'virtualmachine'])), _connector='OR')), related_name='custom_fields', to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='customlink',
+            name='content_type',
+            field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'provider'])), models.Q(('app_label', 'dcim'), ('model__in', ['cable', 'device', 'devicetype', 'powerpanel', 'powerfeed', 'rack', 'site'])), models.Q(('app_label', 'ipam'), ('model__in', ['aggregate', 'ipaddress', 'prefix', 'service', 'vlan', 'vrf'])), models.Q(('app_label', 'secrets'), ('model__in', ['secret'])), models.Q(('app_label', 'tenancy'), ('model__in', ['tenant'])), models.Q(('app_label', 'virtualization'), ('model__in', ['cluster', 'virtualmachine'])), _connector='OR')), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='content_type',
+            field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'provider'])), models.Q(('app_label', 'dcim'), ('model__in', ['cable', 'consoleport', 'device', 'devicetype', 'interface', 'inventoryitem', 'manufacturer', 'powerpanel', 'powerport', 'powerfeed', 'rack', 'rackgroup', 'region', 'site', 'virtualchassis'])), models.Q(('app_label', 'ipam'), ('model__in', ['aggregate', 'ipaddress', 'prefix', 'service', 'vlan', 'vrf'])), models.Q(('app_label', 'secrets'), ('model__in', ['secret'])), models.Q(('app_label', 'tenancy'), ('model__in', ['tenant'])), models.Q(('app_label', 'virtualization'), ('model__in', ['cluster', 'virtualmachine'])), _connector='OR')), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='graph',
+            name='type',
+            field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ['provider'])), models.Q(('app_label', 'dcim'), ('model__in', ['device', 'interface', 'site'])), _connector='OR')), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='webhook',
+            name='obj_type',
+            field=models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'provider'])), models.Q(('app_label', 'dcim'), ('model__in', ['cable', 'consoleport', 'consoleserverport', 'device', 'devicebay', 'devicetype', 'frontport', 'interface', 'inventoryitem', 'manufacturer', 'poweroutlet', 'powerpanel', 'powerport', 'powerfeed', 'rack', 'rearport', 'region', 'site', 'virtualchassis'])), models.Q(('app_label', 'ipam'), ('model__in', ['aggregate', 'ipaddress', 'prefix', 'service', 'vlan', 'vrf'])), models.Q(('app_label', 'secrets'), ('model__in', ['secret'])), models.Q(('app_label', 'tenancy'), ('model__in', ['tenant'])), models.Q(('app_label', 'virtualization'), ('model__in', ['cluster', 'virtualmachine'])), _connector='OR')), related_name='webhooks', to='contenttypes.ContentType'),
+        ),
+    ]

+ 6 - 22
netbox/extras/models.py

@@ -13,7 +13,7 @@ from django.urls import reverse
 from taggit.models import TagBase, GenericTaggedItemBase
 from taggit.models import TagBase, GenericTaggedItemBase
 
 
 from utilities.fields import ColorField
 from utilities.fields import ColorField
-from utilities.utils import deepmerge, model_names_to_filter_dict, render_jinja2
+from utilities.utils import deepmerge, render_jinja2
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *
 from .querysets import ConfigContextQuerySet
 from .querysets import ConfigContextQuerySet
@@ -43,10 +43,6 @@ __all__ = (
 # Webhooks
 # Webhooks
 #
 #
 
 
-def get_webhook_models():
-    return model_names_to_filter_dict(WEBHOOK_MODELS)
-
-
 class Webhook(models.Model):
 class Webhook(models.Model):
     """
     """
     A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
     A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
@@ -58,7 +54,7 @@ class Webhook(models.Model):
         to=ContentType,
         to=ContentType,
         related_name='webhooks',
         related_name='webhooks',
         verbose_name='Object types',
         verbose_name='Object types',
-        limit_choices_to=get_webhook_models,
+        limit_choices_to=WEBHOOK_MODELS,
         help_text="The object(s) to which this Webhook applies."
         help_text="The object(s) to which this Webhook applies."
     )
     )
     name = models.CharField(
     name = models.CharField(
@@ -192,16 +188,12 @@ class CustomFieldModel(models.Model):
             return OrderedDict([(field, None) for field in fields])
             return OrderedDict([(field, None) for field in fields])
 
 
 
 
-def get_custom_field_models():
-    return model_names_to_filter_dict(CUSTOMFIELD_MODELS)
-
-
 class CustomField(models.Model):
 class CustomField(models.Model):
     obj_type = models.ManyToManyField(
     obj_type = models.ManyToManyField(
         to=ContentType,
         to=ContentType,
         related_name='custom_fields',
         related_name='custom_fields',
         verbose_name='Object(s)',
         verbose_name='Object(s)',
-        limit_choices_to=get_custom_field_models,
+        limit_choices_to=CUSTOMFIELD_MODELS,
         help_text='The object(s) to which this field applies.'
         help_text='The object(s) to which this field applies.'
     )
     )
     type = models.CharField(
     type = models.CharField(
@@ -371,10 +363,6 @@ class CustomFieldChoice(models.Model):
 # Custom links
 # Custom links
 #
 #
 
 
-def get_custom_link_models():
-    return model_names_to_filter_dict(CUSTOMLINK_MODELS)
-
-
 class CustomLink(models.Model):
 class CustomLink(models.Model):
     """
     """
     A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
     A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
@@ -383,7 +371,7 @@ class CustomLink(models.Model):
     content_type = models.ForeignKey(
     content_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
-        limit_choices_to=get_custom_link_models
+        limit_choices_to=CUSTOMLINK_MODELS
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=100,
         max_length=100,
@@ -431,7 +419,7 @@ class Graph(models.Model):
     type = models.ForeignKey(
     type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
-        limit_choices_to=model_names_to_filter_dict(GRAPH_MODELS)
+        limit_choices_to=GRAPH_MODELS
     )
     )
     weight = models.PositiveSmallIntegerField(
     weight = models.PositiveSmallIntegerField(
         default=1000
         default=1000
@@ -490,15 +478,11 @@ class Graph(models.Model):
 # Export templates
 # Export templates
 #
 #
 
 
-def get_export_template_models():
-    return model_names_to_filter_dict(EXPORTTEMPLATE_MODELS)
-
-
 class ExportTemplate(models.Model):
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(
     content_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
-        limit_choices_to=get_export_template_models
+        limit_choices_to=EXPORTTEMPLATE_MODELS
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=100
         max_length=100

+ 1 - 2
netbox/extras/tests/test_api.py

@@ -13,7 +13,6 @@ from extras.models import ConfigContext, Graph, ExportTemplate, Tag
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.testing import APITestCase, choices_to_dict
 from utilities.testing import APITestCase, choices_to_dict
-from utilities.utils import model_names_to_filter_dict
 
 
 
 
 class ChoicesTest(APITestCase):
 class ChoicesTest(APITestCase):
@@ -29,7 +28,7 @@ class ChoicesTest(APITestCase):
         self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict())
         self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict())
 
 
         # Graph
         # Graph
-        content_types = ContentType.objects.filter(**model_names_to_filter_dict(GRAPH_MODELS))
+        content_types = ContentType.objects.filter(GRAPH_MODELS)
         graph_type_choices = {
         graph_type_choices = {
             "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
             "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types
         }
         }

+ 5 - 2
netbox/extras/tests/test_filters.py

@@ -3,6 +3,7 @@ from django.test import TestCase
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from extras.choices import *
 from extras.choices import *
+from extras.constants import GRAPH_MODELS
 from extras.filters import *
 from extras.filters import *
 from extras.models import ConfigContext, ExportTemplate, Graph
 from extras.models import ConfigContext, ExportTemplate, Graph
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
@@ -15,7 +16,8 @@ class GraphTestCase(TestCase):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
-        content_types = ContentType.objects.filter(model__in=['site', 'device', 'interface'])
+        # Get the first three available types
+        content_types = ContentType.objects.filter(GRAPH_MODELS)[:3]
 
 
         graphs = (
         graphs = (
             Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
             Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
@@ -29,7 +31,8 @@ class GraphTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_type(self):
     def test_type(self):
-        params = {'type': ContentType.objects.get(model='site').pk}
+        content_type = ContentType.objects.filter(GRAPH_MODELS).first()
+        params = {'type': content_type.pk}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     # TODO: Remove in v2.8
     # TODO: Remove in v2.8

+ 4 - 3
netbox/extras/webhooks.py

@@ -1,6 +1,5 @@
 import datetime
 import datetime
 
 
-from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
 from extras.models import Webhook
 from extras.models import Webhook
@@ -14,7 +13,10 @@ def enqueue_webhooks(instance, user, request_id, action):
     Find Webhook(s) assigned to this instance + action and enqueue them
     Find Webhook(s) assigned to this instance + action and enqueue them
     to be processed
     to be processed
     """
     """
-    if instance._meta.label.lower() not in WEBHOOK_MODELS:
+    obj_type = ContentType.objects.get_for_model(instance.__class__)
+
+    webhook_models = ContentType.objects.filter(WEBHOOK_MODELS)
+    if obj_type not in webhook_models:
         return
         return
 
 
     # Retrieve any applicable Webhooks
     # Retrieve any applicable Webhooks
@@ -23,7 +25,6 @@ def enqueue_webhooks(instance, user, request_id, action):
         ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
         ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
         ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
         ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
     }[action]
     }[action]
-    obj_type = ContentType.objects.get_for_model(instance.__class__)
     webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
     webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
 
 
     if webhooks.exists():
     if webhooks.exists():

+ 0 - 11
netbox/utilities/utils.py

@@ -62,17 +62,6 @@ def dynamic_import(name):
     return mod
     return mod
 
 
 
 
-def model_names_to_filter_dict(names):
-    """
-    Accept a list of content types in the format ['<app>.<model>', '<app>.<model>', ...] and return a dictionary
-    suitable for QuerySet filtering.
-    """
-    # TODO: This should match on the app_label as well as the model name to avoid potential duplicate names
-    return {
-        'model__in': [model.split('.')[1] for model in names],
-    }
-
-
 def get_subquery(model, field):
 def get_subquery(model, field):
     """
     """
     Return a Subquery suitable for annotating a child object count.
     Return a Subquery suitable for annotating a child object count.