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

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

Closes #3892: Robust ContentType filtering
Jeremy Stretch 6 лет назад
Родитель
Сommit
4073dedff8

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

@@ -609,10 +609,10 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
 
 class CableSerializer(ValidatedModelSerializer):
     termination_a_type = ContentTypeField(
-        queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
     )
     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_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
 
 
@@ -43,10 +45,21 @@ CONNECTION_STATUS_CHOICES = [
 ]
 
 # 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 = {
     'consoleport': ['consoleserverport', 'frontport', 'rearport'],

+ 2 - 6
netbox/dcim/forms.py

@@ -3374,9 +3374,7 @@ class CableCSVForm(forms.ModelForm):
     )
     side_a_type = forms.ModelChoiceField(
         queryset=ContentType.objects.all(),
-        limit_choices_to={
-            'model__in': CABLE_TERMINATION_TYPES,
-        },
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         to_field_name='model',
         help_text='Side A type'
     )
@@ -3395,9 +3393,7 @@ class CableCSVForm(forms.ModelForm):
     )
     side_b_type = forms.ModelChoiceField(
         queryset=ContentType.objects.all(),
-        limit_choices_to={
-            'model__in': CABLE_TERMINATION_TYPES,
-        },
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         to_field_name='model',
         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(
         to=ContentType,
-        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         on_delete=models.PROTECT,
         related_name='+'
     )
@@ -1950,7 +1950,7 @@ class Cable(ChangeLoggedModel):
     )
     termination_b_type = models.ForeignKey(
         to=ContentType,
-        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        limit_choices_to=CABLE_TERMINATION_MODELS,
         on_delete=models.PROTECT,
         related_name='+'
     )

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

@@ -30,7 +30,7 @@ class ChoicesTest(APITestCase):
         # Cable
         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())
-        content_types = ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS)
         cable_termination_choices = {
             "{}.{}".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,
     ValidatedModelSerializer,
 )
-from utilities.utils import model_names_to_filter_dict
 from .nested_serializers import *
 
 
@@ -30,7 +29,7 @@ from .nested_serializers import *
 
 class GraphSerializer(ValidatedModelSerializer):
     type = ContentTypeField(
-        queryset=ContentType.objects.filter(**model_names_to_filter_dict(GRAPH_MODELS)),
+        queryset=ContentType.objects.filter(GRAPH_MODELS),
     )
 
     class Meta:

+ 162 - 107
netbox/extras/constants.py

@@ -1,85 +1,128 @@
+from django.db.models import Q
+
+
 # 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
-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
-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
-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
 LOG_DEFAULT = 0
@@ -96,36 +139,48 @@ LOG_LEVEL_CODES = {
 }
 
 # 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)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('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={
                 'ordering': ['group_name', 'weight', 'name'],
@@ -33,16 +33,16 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='customfield',
             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(
             model_name='exporttemplate',
             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(
             model_name='webhook',
             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)),
                 ('button_class', models.CharField(default='default', max_length=30)),
                 ('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={
                 'ordering': ['group_name', 'weight', 'name'],
@@ -115,17 +115,17 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='customfield',
             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(
             model_name='exporttemplate',
             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(
             model_name='webhook',
             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(
             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 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 .constants import *
 from .querysets import ConfigContextQuerySet
@@ -43,10 +43,6 @@ __all__ = (
 # Webhooks
 #
 
-def get_webhook_models():
-    return model_names_to_filter_dict(WEBHOOK_MODELS)
-
-
 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
@@ -58,7 +54,7 @@ class Webhook(models.Model):
         to=ContentType,
         related_name='webhooks',
         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."
     )
     name = models.CharField(
@@ -192,16 +188,12 @@ class CustomFieldModel(models.Model):
             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):
     obj_type = models.ManyToManyField(
         to=ContentType,
         related_name='custom_fields',
         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.'
     )
     type = models.CharField(
@@ -371,10 +363,6 @@ class CustomFieldChoice(models.Model):
 # Custom links
 #
 
-def get_custom_link_models():
-    return model_names_to_filter_dict(CUSTOMLINK_MODELS)
-
-
 class CustomLink(models.Model):
     """
     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(
         to=ContentType,
         on_delete=models.CASCADE,
-        limit_choices_to=get_custom_link_models
+        limit_choices_to=CUSTOMLINK_MODELS
     )
     name = models.CharField(
         max_length=100,
@@ -431,7 +419,7 @@ class Graph(models.Model):
     type = models.ForeignKey(
         to=ContentType,
         on_delete=models.CASCADE,
-        limit_choices_to=model_names_to_filter_dict(GRAPH_MODELS)
+        limit_choices_to=GRAPH_MODELS
     )
     weight = models.PositiveSmallIntegerField(
         default=1000
@@ -490,15 +478,11 @@ class Graph(models.Model):
 # Export templates
 #
 
-def get_export_template_models():
-    return model_names_to_filter_dict(EXPORTTEMPLATE_MODELS)
-
-
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(
         to=ContentType,
         on_delete=models.CASCADE,
-        limit_choices_to=get_export_template_models
+        limit_choices_to=EXPORTTEMPLATE_MODELS
     )
     name = models.CharField(
         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 tenancy.models import Tenant, TenantGroup
 from utilities.testing import APITestCase, choices_to_dict
-from utilities.utils import model_names_to_filter_dict
 
 
 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())
 
         # Graph
-        content_types = ContentType.objects.filter(**model_names_to_filter_dict(GRAPH_MODELS))
+        content_types = ContentType.objects.filter(GRAPH_MODELS)
         graph_type_choices = {
             "{}.{}".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 extras.choices import *
+from extras.constants import GRAPH_MODELS
 from extras.filters import *
 from extras.models import ConfigContext, ExportTemplate, Graph
 from tenancy.models import Tenant, TenantGroup
@@ -15,7 +16,8 @@ class GraphTestCase(TestCase):
     @classmethod
     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 = (
             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)
 
     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)
 
     # TODO: Remove in v2.8

+ 4 - 3
netbox/extras/webhooks.py

@@ -1,6 +1,5 @@
 import datetime
 
-from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 
 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
     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
 
     # Retrieve any applicable Webhooks
@@ -23,7 +25,6 @@ def enqueue_webhooks(instance, user, request_id, action):
         ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
         ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
     }[action]
-    obj_type = ContentType.objects.get_for_model(instance.__class__)
     webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
 
     if webhooks.exists():

+ 0 - 11
netbox/utilities/utils.py

@@ -62,17 +62,6 @@ def dynamic_import(name):
     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):
     """
     Return a Subquery suitable for annotating a child object count.