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

Closes #3892: Robust ContentType filtering
This commit is contained in:
Jeremy Stretch 2020-01-15 16:37:50 -05:00 committed by GitHub
commit 4073dedff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 172 deletions

View File

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

View File

@ -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 = [ CABLE_TERMINATION_MODELS = Q(
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', Q(app_label='circuits', model__in=(
'circuittermination', 'powerfeed', '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'],

View File

@ -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={ limit_choices_to=CABLE_TERMINATION_MODELS,
'model__in': CABLE_TERMINATION_TYPES,
},
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={ limit_choices_to=CABLE_TERMINATION_MODELS,
'model__in': CABLE_TERMINATION_TYPES,
},
to_field_name='model', to_field_name='model',
help_text='Side B type' help_text='Side B type'
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -1,85 +1,128 @@
from django.db.models import Q
# Models which support custom fields # Models which support custom fields
CUSTOMFIELD_MODELS = [ CUSTOMFIELD_MODELS = Q(
'circuits.circuit', Q(app_label='circuits', model__in=[
'circuits.provider', 'circuit',
'dcim.device', 'provider',
'dcim.devicetype', ]) |
'dcim.powerfeed', Q(app_label='dcim', model__in=[
'dcim.rack', 'device',
'dcim.site', 'devicetype',
'ipam.aggregate', 'powerfeed',
'ipam.ipaddress', 'rack',
'ipam.prefix', 'site',
'ipam.service', ]) |
'ipam.vlan', Q(app_label='ipam', model__in=[
'ipam.vrf', 'aggregate',
'secrets.secret', 'ipaddress',
'tenancy.tenant', 'prefix',
'virtualization.cluster', 'service',
'virtualization.virtualmachine', '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 = [ CUSTOMLINK_MODELS = Q(
'circuits.circuit', Q(app_label='circuits', model__in=[
'circuits.provider', 'circuit',
'dcim.cable', 'provider',
'dcim.device', ]) |
'dcim.devicetype', Q(app_label='dcim', model__in=[
'dcim.powerpanel', 'cable',
'dcim.powerfeed', 'device',
'dcim.rack', 'devicetype',
'dcim.site', 'powerpanel',
'ipam.aggregate', 'powerfeed',
'ipam.ipaddress', 'rack',
'ipam.prefix', 'site',
'ipam.service', ]) |
'ipam.vlan', Q(app_label='ipam', model__in=[
'ipam.vrf', 'aggregate',
'secrets.secret', 'ipaddress',
'tenancy.tenant', 'prefix',
'virtualization.cluster', 'service',
'virtualization.virtualmachine', '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 = ( GRAPH_MODELS = Q(
'circuits.provider', Q(app_label='circuits', model__in=[
'dcim.device', 'provider',
'dcim.interface', ]) |
'dcim.site', Q(app_label='dcim', model__in=[
'device',
'interface',
'site',
])
) )
# Models which support export templates # Models which support export templates
EXPORTTEMPLATE_MODELS = [ EXPORTTEMPLATE_MODELS = Q(
'circuits.circuit', Q(app_label='circuits', model__in=[
'circuits.provider', 'circuit',
'dcim.cable', 'provider',
'dcim.consoleport', ]) |
'dcim.device', Q(app_label='dcim', model__in=[
'dcim.devicetype', 'cable',
'dcim.interface', 'consoleport',
'dcim.inventoryitem', 'device',
'dcim.manufacturer', 'devicetype',
'dcim.powerpanel', 'interface',
'dcim.powerport', 'inventoryitem',
'dcim.powerfeed', 'manufacturer',
'dcim.rack', 'powerpanel',
'dcim.rackgroup', 'powerport',
'dcim.region', 'powerfeed',
'dcim.site', 'rack',
'dcim.virtualchassis', 'rackgroup',
'ipam.aggregate', 'region',
'ipam.ipaddress', 'site',
'ipam.prefix', 'virtualchassis',
'ipam.service', ]) |
'ipam.vlan', Q(app_label='ipam', model__in=[
'ipam.vrf', 'aggregate',
'secrets.secret', 'ipaddress',
'tenancy.tenant', 'prefix',
'virtualization.cluster', 'service',
'virtualization.virtualmachine', '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 = [ WEBHOOK_MODELS = Q(
'circuits.circuit', Q(app_label='circuits', model__in=[
'circuits.provider', 'circuit',
'dcim.cable', 'provider',
'dcim.consoleport', ]) |
'dcim.consoleserverport', Q(app_label='dcim', model__in=[
'dcim.device', 'cable',
'dcim.devicebay', 'consoleport',
'dcim.devicetype', 'consoleserverport',
'dcim.interface', 'device',
'dcim.inventoryitem', 'devicebay',
'dcim.frontport', 'devicetype',
'dcim.manufacturer', 'frontport',
'dcim.poweroutlet', 'interface',
'dcim.powerpanel', 'inventoryitem',
'dcim.powerport', 'manufacturer',
'dcim.powerfeed', 'poweroutlet',
'dcim.rack', 'powerpanel',
'dcim.rearport', 'powerport',
'dcim.region', 'powerfeed',
'dcim.site', 'rack',
'dcim.virtualchassis', 'rearport',
'ipam.aggregate', 'region',
'ipam.ipaddress', 'site',
'ipam.prefix', 'virtualchassis',
'ipam.service', ]) |
'ipam.vlan', Q(app_label='ipam', model__in=[
'ipam.vrf', 'aggregate',
'secrets.secret', 'ipaddress',
'tenancy.tenant', 'prefix',
'virtualization.cluster', 'service',
'virtualization.virtualmachine', 'vlan',
] 'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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