Closes #22109: Add template object counts to ModuleType representation in REST & GraphQL APIs (#22302)
This commit is contained in:
parent
d13f5e8214
commit
7022bb7eac
|
|
@ -11,8 +11,9 @@ Similar to [device types](./devicetype.md), each module type can have any of the
|
|||
* Power Outlets
|
||||
* Front pass-through ports
|
||||
* Rear pass-through ports
|
||||
* Module bays
|
||||
|
||||
Note that device bays and module bays may _not_ be added to modules.
|
||||
Note that device bays may _not_ be added to modules.
|
||||
|
||||
## Automatic Component Renaming
|
||||
|
||||
|
|
|
|||
|
|
@ -101,11 +101,24 @@ class ModuleTypeSerializer(PrimaryModelSerializer):
|
|||
)
|
||||
module_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Counter fields
|
||||
console_port_template_count = serializers.IntegerField(read_only=True)
|
||||
console_server_port_template_count = serializers.IntegerField(read_only=True)
|
||||
power_port_template_count = serializers.IntegerField(read_only=True)
|
||||
power_outlet_template_count = serializers.IntegerField(read_only=True)
|
||||
interface_template_count = serializers.IntegerField(read_only=True)
|
||||
front_port_template_count = serializers.IntegerField(read_only=True)
|
||||
rear_port_template_count = serializers.IntegerField(read_only=True)
|
||||
module_bay_template_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
|
||||
'weight', 'weight_unit', 'description', 'attributes', 'owner', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'module_count',
|
||||
'created', 'last_updated', 'module_count', 'console_port_template_count',
|
||||
'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count',
|
||||
'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
|
||||
'module_bay_template_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description', 'module_count')
|
||||
|
|
|
|||
|
|
@ -862,6 +862,10 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
|||
method='_pass_through_ports',
|
||||
label=_('Has pass-through ports'),
|
||||
)
|
||||
module_bays = django_filters.BooleanFilter(
|
||||
method='_module_bays',
|
||||
label=_('Has module bays'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
|
|
@ -869,6 +873,14 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
|||
'id', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description',
|
||||
|
||||
# Counters
|
||||
'console_port_template_count',
|
||||
'console_server_port_template_count',
|
||||
'power_port_template_count',
|
||||
'power_outlet_template_count',
|
||||
'interface_template_count',
|
||||
'front_port_template_count',
|
||||
'rear_port_template_count',
|
||||
'module_bay_template_count',
|
||||
'module_count',
|
||||
)
|
||||
|
||||
|
|
@ -904,6 +916,9 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
|
|||
rearporttemplates__isnull=value
|
||||
)
|
||||
|
||||
def _module_bays(self, queryset, name, value):
|
||||
return queryset.exclude(modulebaytemplates__isnull=value)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||
),
|
||||
FieldSet(
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
'pass_through_ports', name=_('Components')
|
||||
'pass_through_ports', 'module_bays', name=_('Components')
|
||||
),
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
|
|
@ -778,6 +778,13 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
|
|||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
module_bays = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has module bays'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
label=_('Airflow'),
|
||||
|
|
|
|||
|
|
@ -644,6 +644,14 @@ class ModuleTypeProfileType(PrimaryObjectType):
|
|||
)
|
||||
class ModuleTypeType(PrimaryObjectType):
|
||||
module_count: BigInt
|
||||
console_port_template_count: BigInt
|
||||
console_server_port_template_count: BigInt
|
||||
power_port_template_count: BigInt
|
||||
power_outlet_template_count: BigInt
|
||||
interface_template_count: BigInt
|
||||
front_port_template_count: BigInt
|
||||
rear_port_template_count: BigInt
|
||||
module_bay_template_count: BigInt
|
||||
profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
from django.db import migrations
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
|
||||
import utilities.fields
|
||||
|
||||
|
||||
def populate_module_type_component_counts(apps, schema_editor):
|
||||
"""
|
||||
Populate the component template counter fields for existing ModuleTypes.
|
||||
"""
|
||||
ModuleType = apps.get_model('dcim', 'ModuleType')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
counters = {
|
||||
'console_port_template_count': 'consoleporttemplates',
|
||||
'console_server_port_template_count': 'consoleserverporttemplates',
|
||||
'power_port_template_count': 'powerporttemplates',
|
||||
'power_outlet_template_count': 'poweroutlettemplates',
|
||||
'interface_template_count': 'interfacetemplates',
|
||||
'front_port_template_count': 'frontporttemplates',
|
||||
'rear_port_template_count': 'rearporttemplates',
|
||||
'module_bay_template_count': 'modulebaytemplates',
|
||||
}
|
||||
|
||||
for field_name, related_name in counters.items():
|
||||
count_subquery = (
|
||||
ModuleType.objects.using(db_alias)
|
||||
.filter(pk=OuterRef('pk'))
|
||||
.annotate(_count=Count(related_name))
|
||||
.values('_count')
|
||||
)
|
||||
ModuleType.objects.using(db_alias).update(**{field_name: Subquery(count_subquery)})
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0235_cabletermination_circuit_site_cache'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='console_port_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.ConsolePortTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='console_server_port_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.ConsoleServerPortTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='front_port_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.FrontPortTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='interface_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.InterfaceTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='module_bay_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.ModuleBayTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='power_outlet_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.PowerOutletTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='power_port_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.PowerPortTemplate'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='rear_port_template_count',
|
||||
field=utilities.fields.CounterCacheField(
|
||||
default=0, editable=False, to_field='module_type', to_model='dcim.RearPortTemplate'
|
||||
),
|
||||
),
|
||||
migrations.RunPython(populate_module_type_component_counts, migrations.RunPython.noop),
|
||||
]
|
||||
|
|
@ -58,8 +58,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||
"""
|
||||
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
||||
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
||||
DeviceType, each ModuleType can have console, power, interface, and pass-through port templates assigned to it. It
|
||||
cannot, however house device bays or module bays.
|
||||
DeviceType, each ModuleType can have console, power, interface, pass-through port, and module bay templates assigned
|
||||
to it. It cannot, however, house device bays.
|
||||
"""
|
||||
profile = models.ForeignKey(
|
||||
to='dcim.ModuleTypeProfile',
|
||||
|
|
@ -100,6 +100,40 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||
to_field='module_type'
|
||||
)
|
||||
|
||||
# Counter fields
|
||||
console_port_template_count = CounterCacheField(
|
||||
to_model='dcim.ConsolePortTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
console_server_port_template_count = CounterCacheField(
|
||||
to_model='dcim.ConsoleServerPortTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
power_port_template_count = CounterCacheField(
|
||||
to_model='dcim.PowerPortTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
power_outlet_template_count = CounterCacheField(
|
||||
to_model='dcim.PowerOutletTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
interface_template_count = CounterCacheField(
|
||||
to_model='dcim.InterfaceTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
front_port_template_count = CounterCacheField(
|
||||
to_model='dcim.FrontPortTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
rear_port_template_count = CounterCacheField(
|
||||
to_model='dcim.RearPortTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
module_bay_template_count = CounterCacheField(
|
||||
to_model='dcim.ModuleBayTemplate',
|
||||
to_field='module_type'
|
||||
)
|
||||
|
||||
clone_fields = ('profile', 'manufacturer', 'weight', 'weight_unit', 'airflow')
|
||||
prerequisite_models = (
|
||||
'dcim.Manufacturer',
|
||||
|
|
|
|||
|
|
@ -1707,6 +1707,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
|
||||
PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
|
||||
])
|
||||
ModuleBayTemplate.objects.bulk_create((
|
||||
ModuleBayTemplate(module_type=module_types[0], name='Module Bay 1'),
|
||||
ModuleBayTemplate(module_type=module_types[1], name='Module Bay 2'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
|
|
@ -1767,6 +1771,12 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'pass_through_ports': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_module_bays(self):
|
||||
params = {'module_bays': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'module_bays': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_weight(self):
|
||||
params = {'weight': [10, 20]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,71 @@ class LocationTestCase(TestCase):
|
|||
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(TestCase):
|
||||
|
||||
def test_component_template_counts(self):
|
||||
"""
|
||||
DeviceType component template counters should track the addition and removal of templates.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
|
||||
)
|
||||
|
||||
# Counters should start at zero
|
||||
self.assertEqual(device_type.interface_template_count, 0)
|
||||
self.assertEqual(device_type.console_port_template_count, 0)
|
||||
self.assertEqual(device_type.module_bay_template_count, 0)
|
||||
self.assertEqual(device_type.device_bay_template_count, 0)
|
||||
|
||||
# Adding templates should increment the relevant counters
|
||||
InterfaceTemplate.objects.create(device_type=device_type, name='Interface 1')
|
||||
InterfaceTemplate.objects.create(device_type=device_type, name='Interface 2')
|
||||
ConsolePortTemplate.objects.create(device_type=device_type, name='Console 1')
|
||||
ModuleBayTemplate.objects.create(device_type=device_type, name='Module Bay 1')
|
||||
DeviceBayTemplate.objects.create(device_type=device_type, name='Device Bay 1')
|
||||
device_type.refresh_from_db()
|
||||
self.assertEqual(device_type.interface_template_count, 2)
|
||||
self.assertEqual(device_type.console_port_template_count, 1)
|
||||
self.assertEqual(device_type.module_bay_template_count, 1)
|
||||
self.assertEqual(device_type.device_bay_template_count, 1)
|
||||
|
||||
# Deleting a template should decrement the counter
|
||||
InterfaceTemplate.objects.get(device_type=device_type, name='Interface 1').delete()
|
||||
device_type.refresh_from_db()
|
||||
self.assertEqual(device_type.interface_template_count, 1)
|
||||
|
||||
|
||||
class ModuleTypeTestCase(TestCase):
|
||||
|
||||
def test_component_template_counts(self):
|
||||
"""
|
||||
ModuleType component template counters should track the addition and removal of templates.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||
|
||||
# Counters should start at zero
|
||||
self.assertEqual(module_type.interface_template_count, 0)
|
||||
self.assertEqual(module_type.console_port_template_count, 0)
|
||||
self.assertEqual(module_type.module_bay_template_count, 0)
|
||||
|
||||
# Adding templates should increment the relevant counters
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='Interface 1')
|
||||
InterfaceTemplate.objects.create(module_type=module_type, name='Interface 2')
|
||||
ConsolePortTemplate.objects.create(module_type=module_type, name='Console 1')
|
||||
ModuleBayTemplate.objects.create(module_type=module_type, name='Module Bay 1')
|
||||
module_type.refresh_from_db()
|
||||
self.assertEqual(module_type.interface_template_count, 2)
|
||||
self.assertEqual(module_type.console_port_template_count, 1)
|
||||
self.assertEqual(module_type.module_bay_template_count, 1)
|
||||
|
||||
# Deleting a template should decrement the counter
|
||||
InterfaceTemplate.objects.get(module_type=module_type, name='Interface 1').delete()
|
||||
module_type.refresh_from_db()
|
||||
self.assertEqual(module_type.interface_template_count, 1)
|
||||
|
||||
|
||||
class RackTypeTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -1810,7 +1810,7 @@ class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_consoleports'
|
||||
tab = ViewTab(
|
||||
label=_('Console Ports'),
|
||||
badge=lambda obj: obj.consoleporttemplates.count(),
|
||||
badge=lambda obj: obj.console_port_template_count,
|
||||
permission='dcim.view_consoleporttemplate',
|
||||
weight=530,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1825,7 +1825,7 @@ class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_consoleserverports'
|
||||
tab = ViewTab(
|
||||
label=_('Console Server Ports'),
|
||||
badge=lambda obj: obj.consoleserverporttemplates.count(),
|
||||
badge=lambda obj: obj.console_server_port_template_count,
|
||||
permission='dcim.view_consoleserverporttemplate',
|
||||
weight=540,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1840,7 +1840,7 @@ class ModuleTypePowerPortsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_powerports'
|
||||
tab = ViewTab(
|
||||
label=_('Power Ports'),
|
||||
badge=lambda obj: obj.powerporttemplates.count(),
|
||||
badge=lambda obj: obj.power_port_template_count,
|
||||
permission='dcim.view_powerporttemplate',
|
||||
weight=550,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1855,7 +1855,7 @@ class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_poweroutlets'
|
||||
tab = ViewTab(
|
||||
label=_('Power Outlets'),
|
||||
badge=lambda obj: obj.poweroutlettemplates.count(),
|
||||
badge=lambda obj: obj.power_outlet_template_count,
|
||||
permission='dcim.view_poweroutlettemplate',
|
||||
weight=560,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1870,7 +1870,7 @@ class ModuleTypeInterfacesView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_interfaces'
|
||||
tab = ViewTab(
|
||||
label=_('Interfaces'),
|
||||
badge=lambda obj: obj.interfacetemplates.count(),
|
||||
badge=lambda obj: obj.interface_template_count,
|
||||
permission='dcim.view_interfacetemplate',
|
||||
weight=500,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1885,7 +1885,7 @@ class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_frontports'
|
||||
tab = ViewTab(
|
||||
label=_('Front Ports'),
|
||||
badge=lambda obj: obj.frontporttemplates.count(),
|
||||
badge=lambda obj: obj.front_port_template_count,
|
||||
permission='dcim.view_frontporttemplate',
|
||||
weight=510,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1900,7 +1900,7 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_rearports'
|
||||
tab = ViewTab(
|
||||
label=_('Rear Ports'),
|
||||
badge=lambda obj: obj.rearporttemplates.count(),
|
||||
badge=lambda obj: obj.rear_port_template_count,
|
||||
permission='dcim.view_rearporttemplate',
|
||||
weight=520,
|
||||
hide_if_empty=True
|
||||
|
|
@ -1915,7 +1915,7 @@ class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
|
|||
viewname = 'dcim:moduletype_modulebays'
|
||||
tab = ViewTab(
|
||||
label=_('Module Bays'),
|
||||
badge=lambda obj: obj.modulebaytemplates.count(),
|
||||
badge=lambda obj: obj.module_bay_template_count,
|
||||
permission='dcim.view_modulebaytemplate',
|
||||
weight=570,
|
||||
hide_if_empty=True
|
||||
|
|
|
|||
Loading…
Reference in New Issue