mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #3932 from netbox-community/3892-contenttype-filtering
Closes #3892: Robust ContentType filtering
This commit is contained in:
commit
4073dedff8
@ -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)
|
||||||
|
@ -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'],
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
24
netbox/dcim/migrations/0090_cable_termination_models.py
Normal file
24
netbox/dcim/migrations/0090_cable_termination_models.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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='+'
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
@ -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'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -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)",
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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():
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user