diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 4486ecd1b..1053287f7 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -3,8 +3,8 @@ from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
from utilities.tables import (
- BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, TagColumn,
- ToggleColumn,
+ BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
+ TagColumn, ToggleColumn,
)
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -49,14 +49,6 @@ RACKGROUP_ELEVATIONS = """
"""
-RACK_DEVICE_COUNT = """
-{{ value }}
-"""
-
-DEVICE_COUNT = """
-{{ value|default:0 }}
-"""
-
RACKRESERVATION_ACTIONS = """
@@ -75,14 +67,6 @@ MANUFACTURER_ACTIONS = """
{% endif %}
"""
-DEVICEROLE_DEVICE_COUNT = """
-{{ value|default:0 }}
-"""
-
-DEVICEROLE_VM_COUNT = """
-{{ value|default:0 }}
-"""
-
DEVICEROLE_ACTIONS = """
@@ -92,24 +76,12 @@ DEVICEROLE_ACTIONS = """
{% endif %}
"""
-PLATFORM_DEVICE_COUNT = """
-{{ value|default:0 }}
-"""
-
-PLATFORM_VM_COUNT = """
-{{ value|default:0 }}
-"""
-
DEVICE_PRIMARY_IP = """
{{ record.primary_ip6.address.ip|default:"" }}
{% if record.primary_ip6 and record.primary_ip4 %}
{% endif %}
{{ record.primary_ip4.address.ip|default:"" }}
"""
-DEVICETYPE_INSTANCES_TEMPLATE = """
-{{ record.instance_count }}
-"""
-
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
@@ -129,10 +101,6 @@ CABLE_LENGTH = """
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
"""
-POWERPANEL_POWERFEED_COUNT = """
-{{ value }}
-"""
-
INTERFACE_IPADDRESSES = """
{% for ip in record.ip_addresses.unrestricted %}
{{ ip }}
@@ -280,8 +248,9 @@ class RackTable(BaseTable):
class RackDetailTable(RackTable):
- device_count = tables.TemplateColumn(
- template_code=RACK_DEVICE_COUNT,
+ device_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'rack_id': 'pk'},
verbose_name='Devices'
)
get_utilization = tables.TemplateColumn(
@@ -388,8 +357,9 @@ class DeviceTypeTable(BaseTable):
is_full_depth = BooleanColumn(
verbose_name='Full Depth'
)
- instance_count = tables.TemplateColumn(
- template_code=DEVICETYPE_INSTANCES_TEMPLATE,
+ instance_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'device_type_id': 'pk'},
verbose_name='Instances'
)
tags = TagColumn(
@@ -526,12 +496,14 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
- device_count = tables.TemplateColumn(
- template_code=DEVICEROLE_DEVICE_COUNT,
+ device_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'role': 'slug'},
verbose_name='Devices'
)
- vm_count = tables.TemplateColumn(
- template_code=DEVICEROLE_VM_COUNT,
+ vm_count = LinkedCountColumn(
+ viewname='virtualization:virtualmachine_list',
+ url_params={'role': 'slug'},
verbose_name='VMs'
)
color = tables.TemplateColumn(
@@ -553,12 +525,14 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable):
pk = ToggleColumn()
- device_count = tables.TemplateColumn(
- template_code=PLATFORM_DEVICE_COUNT,
+ device_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'platform': 'slug'},
verbose_name='Devices'
)
- vm_count = tables.TemplateColumn(
- template_code=PLATFORM_VM_COUNT,
+ vm_count = LinkedCountColumn(
+ viewname='virtualization:virtualmachine_list',
+ url_params={'platform': 'slug'},
verbose_name='VMs'
)
actions = ButtonsColumn(Platform, pk_field='slug')
@@ -994,7 +968,9 @@ class VirtualChassisTable(BaseTable):
master = tables.Column(
linkify=True
)
- member_count = tables.Column(
+ member_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'virtual_chassis_id': 'pk'},
verbose_name='Members'
)
tags = TagColumn(
@@ -1018,8 +994,9 @@ class PowerPanelTable(BaseTable):
viewname='dcim:site',
args=[Accessor('site__slug')]
)
- powerfeed_count = tables.TemplateColumn(
- template_code=POWERPANEL_POWERFEED_COUNT,
+ powerfeed_count = LinkedCountColumn(
+ viewname='dcim:powerfeed_list',
+ url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
tags = TagColumn(
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index e8b2474ea..1d2ff9243 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -4,7 +4,9 @@ from django_tables2.utils import Accessor
from dcim.models import Interface
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
+from utilities.tables import (
+ BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn,
+)
from virtualization.models import VMInterface
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
@@ -34,14 +36,6 @@ UTILIZATION_GRAPH = """
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
"""
-ROLE_PREFIX_COUNT = """
-{{ value|default:0 }}
-"""
-
-ROLE_VLAN_COUNT = """
-{{ value|default:0 }}
-"""
-
PREFIX_LINK = """
{% if record.children %}
@@ -209,7 +203,9 @@ class RIRTable(BaseTable):
is_private = BooleanColumn(
verbose_name='Private'
)
- aggregate_count = tables.Column(
+ aggregate_count = LinkedCountColumn(
+ viewname='ipam:aggregate_list',
+ url_params={'rir': 'slug'},
verbose_name='Aggregates'
)
actions = ButtonsColumn(RIR, pk_field='slug')
@@ -304,12 +300,14 @@ class AggregateDetailTable(AggregateTable):
class RoleTable(BaseTable):
pk = ToggleColumn()
- prefix_count = tables.TemplateColumn(
- template_code=ROLE_PREFIX_COUNT,
+ prefix_count = LinkedCountColumn(
+ viewname='ipam:prefix_list',
+ url_params={'role': 'slug'},
verbose_name='Prefixes'
)
- vlan_count = tables.TemplateColumn(
- template_code=ROLE_VLAN_COUNT,
+ vlan_count = LinkedCountColumn(
+ viewname='ipam:vlan_list',
+ url_params={'role': 'slug'},
verbose_name='VLANs'
)
actions = ButtonsColumn(Role, pk_field='slug')
@@ -508,7 +506,9 @@ class VLANGroupTable(BaseTable):
viewname='dcim:site',
args=[Accessor('site__slug')]
)
- vlan_count = tables.Column(
+ vlan_count = LinkedCountColumn(
+ viewname='ipam:vlan_list',
+ url_params={'group': 'slug'},
verbose_name='VLANs'
)
actions = ButtonsColumn(
diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py
index 7158b0b13..345c8a689 100644
--- a/netbox/secrets/tables.py
+++ b/netbox/secrets/tables.py
@@ -1,6 +1,6 @@
import django_tables2 as tables
-from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, TagColumn, ToggleColumn
from .models import SecretRole, Secret
@@ -11,7 +11,9 @@ from .models import SecretRole, Secret
class SecretRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
- secret_count = tables.Column(
+ secret_count = LinkedCountColumn(
+ viewname='secrets:secret_list',
+ url_params={'role': 'slug'},
verbose_name='Secrets'
)
actions = ButtonsColumn(SecretRole, pk_field='slug')
diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py
index dc96b839c..8f9073025 100644
--- a/netbox/tenancy/tables.py
+++ b/netbox/tenancy/tables.py
@@ -1,6 +1,6 @@
import django_tables2 as tables
-from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, TagColumn, ToggleColumn
from .models import Tenant, TenantGroup
MPTT_LINK = """
@@ -32,7 +32,9 @@ class TenantGroupTable(BaseTable):
template_code=MPTT_LINK,
orderable=False
)
- tenant_count = tables.Column(
+ tenant_count = LinkedCountColumn(
+ viewname='tenancy:tenant_list',
+ url_params={'group': 'slug'},
verbose_name='Tenants'
)
actions = ButtonsColumn(TenantGroup, pk_field='slug')
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py
index 76c37b403..d4861c93a 100644
--- a/netbox/utilities/tables.py
+++ b/netbox/utilities/tables.py
@@ -1,6 +1,7 @@
import django_tables2 as tables
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
+from django.urls import reverse
from django.utils.safestring import mark_safe
from django_tables2.data import TableQuerysetData
@@ -213,6 +214,29 @@ class ColoredLabelColumn(tables.TemplateColumn):
super().__init__(template_code=self.template_code, *args, **kwargs)
+class LinkedCountColumn(tables.Column):
+ """
+ Render a count of related objects linked to a filtered URL.
+
+ :param viewname: The view name to use for URL resolution
+ :param view_kwargs: Additional kwargs to pass for URL resolution (optional)
+ :param url_params: A dict of query parameters to append to the URL (e.g. ?foo=bar) (optional)
+ """
+ def __init__(self, viewname, *args, view_kwargs=None, url_params=None, default=0, **kwargs):
+ self.viewname = viewname
+ self.view_kwargs = view_kwargs or {}
+ self.url_params = url_params
+ super().__init__(*args, default=default, **kwargs)
+
+ def render(self, record, value):
+ if value:
+ url = reverse(self.viewname, kwargs=self.view_kwargs)
+ if self.url_params:
+ url += '?' + '&'.join([f'{k}={getattr(record, v)}' for k, v in self.url_params.items()])
+ return mark_safe(f'{value}')
+ return value
+
+
class TagColumn(tables.TemplateColumn):
"""
Display a list of tags assigned to the object.
diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py
index 7cb684dee..81cf986d8 100644
--- a/netbox/virtualization/tables.py
+++ b/netbox/virtualization/tables.py
@@ -2,7 +2,9 @@ import django_tables2 as tables
from dcim.tables import BaseInterfaceTable
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, TagColumn, ToggleColumn
+from utilities.tables import (
+ BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, ToggleColumn,
+)
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
VIRTUALMACHINE_PRIMARY_IP = """
@@ -11,14 +13,6 @@ VIRTUALMACHINE_PRIMARY_IP = """
{{ record.primary_ip4.address.ip|default:"" }}
"""
-DEVICE_COUNT = """
-{{ value|default:0 }}
-"""
-
-VM_COUNT = """
-{{ value|default:0 }}
-"""
-
#
# Cluster types
@@ -69,12 +63,14 @@ class ClusterTable(BaseTable):
site = tables.Column(
linkify=True
)
- device_count = tables.TemplateColumn(
- template_code=DEVICE_COUNT,
+ device_count = LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'cluster_id': 'pk'},
verbose_name='Devices'
)
- vm_count = tables.TemplateColumn(
- template_code=VM_COUNT,
+ vm_count = LinkedCountColumn(
+ viewname='virtualization:virtualmachine_list',
+ url_params={'cluster_id': 'pk'},
verbose_name='VMs'
)
tags = TagColumn(