diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 372378a1f..8166fa4b5 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -6,6 +6,7 @@
* [#492](https://github.com/netbox-community/netbox/issues/492) - Enable toggling and rearranging table columns
* [#3147](https://github.com/netbox-community/netbox/issues/3147) - Allow specifying related objects by arbitrary attribute during CSV import
+* [#3064](https://github.com/netbox-community/netbox/issues/3064) - Include tags in object lists as a toggleable table column
* [#3294](https://github.com/netbox-community/netbox/issues/3294) - Implement mechanism for storing user preferences
* [#4421](https://github.com/netbox-community/netbox/issues/4421) - Retain user's preference for config context format
* [#4502](https://github.com/netbox-community/netbox/issues/4502) - Enable configuration of proxies for outbound HTTP requests
diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py
index 0878279ec..ea17031a1 100644
--- a/netbox/circuits/tables.py
+++ b/netbox/circuits/tables.py
@@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """
@@ -31,10 +31,15 @@ class ProviderTable(BaseTable):
accessor=Accessor('count_circuits'),
verbose_name='Circuits'
)
+ tags = TagColumn(
+ url_name='circuits:provider_list'
+ )
class Meta(BaseTable.Meta):
model = Provider
- fields = ('pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count')
+ fields = (
+ 'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags',
+ )
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@@ -45,7 +50,9 @@ class ProviderTable(BaseTable):
class CircuitTypeTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
- circuit_count = tables.Column(verbose_name='Circuits')
+ circuit_count = tables.Column(
+ verbose_name='Circuits'
+ )
actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
@@ -64,21 +71,33 @@ class CircuitTypeTable(BaseTable):
class CircuitTable(BaseTable):
pk = ToggleColumn()
- cid = tables.LinkColumn(verbose_name='ID')
- provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
- status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
- tenant = tables.TemplateColumn(template_code=COL_TENANT)
+ cid = tables.LinkColumn(
+ verbose_name='ID'
+ )
+ provider = tables.LinkColumn(
+ viewname='circuits:provider',
+ args=[Accessor('provider.slug')]
+ )
+ status = tables.TemplateColumn(
+ template_code=STATUS_LABEL
+ )
+ tenant = tables.TemplateColumn(
+ template_code=COL_TENANT
+ )
a_side = tables.Column(
verbose_name='A Side'
)
z_side = tables.Column(
verbose_name='Z Side'
)
+ tags = TagColumn(
+ url_name='circuits:circuit_list'
+ )
class Meta(BaseTable.Meta):
model = Circuit
fields = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate',
- 'description',
+ 'description', 'tags',
)
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 5ce96841f..3fef86394 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
+from utilities.tables import BaseTable, BooleanColumn, ColorColumn, TagColumn, ToggleColumn
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -242,13 +242,16 @@ class SiteTable(BaseTable):
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
+ tags = TagColumn(
+ url_name='dcim:site_list'
+ )
class Meta(BaseTable.Meta):
model = Site
fields = (
'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
- 'contact_email',
+ 'contact_email', 'tags',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
@@ -354,11 +357,14 @@ class RackDetailTable(RackTable):
orderable=False,
verbose_name='Power'
)
+ tags = TagColumn(
+ url_name='dcim:rack_list'
+ )
class Meta(RackTable.Meta):
fields = (
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
- 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization',
+ 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
)
default_columns = (
'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
@@ -450,17 +456,22 @@ class DeviceTypeTable(BaseTable):
args=[Accessor('pk')],
verbose_name='Device Type'
)
- is_full_depth = BooleanColumn(verbose_name='Full Depth')
+ is_full_depth = BooleanColumn(
+ verbose_name='Full Depth'
+ )
instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances'
)
+ tags = TagColumn(
+ url_name='dcim:devicetype_list'
+ )
class Meta(BaseTable.Meta):
model = DeviceType
fields = (
'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
- 'instance_count',
+ 'instance_count', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@@ -834,13 +845,16 @@ class DeviceTable(BaseTable):
vc_priority = tables.Column(
verbose_name='VC Priority'
)
+ tags = TagColumn(
+ url_name='dcim:device_list'
+ )
class Meta(BaseTable.Meta):
model = Device
fields = (
'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
- 'vc_position', 'vc_priority',
+ 'vc_position', 'vc_priority', 'tags',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
@@ -1206,10 +1220,13 @@ class VirtualChassisTable(BaseTable):
member_count = tables.Column(
verbose_name='Members'
)
+ tags = TagColumn(
+ url_name='dcim:virtualchassis_list'
+ )
class Meta(BaseTable.Meta):
model = VirtualChassis
- fields = ('pk', 'name', 'domain', 'member_count')
+ fields = ('pk', 'name', 'domain', 'member_count', 'tags')
default_columns = ('pk', 'name', 'domain', 'member_count')
@@ -1262,12 +1279,15 @@ class PowerFeedTable(BaseTable):
available_power = tables.Column(
verbose_name='Available power (VA)'
)
+ tags = TagColumn(
+ url_name='dcim:powerfeed_list'
+ )
class Meta(BaseTable.Meta):
model = PowerFeed
fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
- 'max_utilization', 'available_power',
+ 'max_utilization', 'available_power', 'tags',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index 56729f9db..23bf14653 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
+from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """
@@ -199,10 +199,13 @@ class VRFTable(BaseTable):
enforce_unique = BooleanColumn(
verbose_name='Unique'
)
+ tags = TagColumn(
+ url_name='ipam:vrf_list'
+ )
class Meta(BaseTable.Meta):
model = VRF
- fields = ('pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description')
+ fields = ('pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags')
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@@ -300,9 +303,13 @@ class AggregateDetailTable(AggregateTable):
template_code=UTILIZATION_GRAPH,
orderable=False
)
+ tags = TagColumn(
+ url_name='ipam:aggregate_list'
+ )
class Meta(AggregateTable.Meta):
- fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
+ fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description', 'tags')
+ default_columns = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
#
@@ -388,10 +395,14 @@ class PrefixDetailTable(PrefixTable):
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
+ tags = TagColumn(
+ url_name='ipam:prefix_list'
+ )
class Meta(PrefixTable.Meta):
fields = (
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
+ 'tags',
)
default_columns = (
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
@@ -446,11 +457,14 @@ class IPAddressDetailTable(IPAddressTable):
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
+ tags = TagColumn(
+ url_name='ipam:ipaddress_list'
+ )
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
- 'description',
+ 'description', 'tags',
)
default_columns = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
@@ -573,9 +587,13 @@ class VLANDetailTable(VLANTable):
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
+ tags = TagColumn(
+ url_name='ipam:vlan_list'
+ )
class Meta(VLANTable.Meta):
- fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
+ fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags')
+ default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
class VLANMemberTable(BaseTable):
@@ -647,8 +665,11 @@ class ServiceTable(BaseTable):
viewname='ipam:service',
args=[Accessor('pk')]
)
+ tags = TagColumn(
+ url_name='ipam:service_list'
+ )
class Meta(BaseTable.Meta):
model = Service
- fields = ('pk', 'name', 'parent', 'protocol', 'port', 'ipaddresses', 'description')
+ fields = ('pk', 'name', 'parent', 'protocol', 'port', 'ipaddresses', 'description', 'tags')
default_columns = ('pk', 'name', 'parent', 'protocol', 'port', 'description')
diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py
index 11646f5de..f92c9216b 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, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
from .models import SecretRole, Secret
SECRETROLE_ACTIONS = """
@@ -42,8 +42,11 @@ class SecretRoleTable(BaseTable):
class SecretTable(BaseTable):
pk = ToggleColumn()
device = tables.LinkColumn()
+ tags = TagColumn(
+ url_name='secrets:secret_list'
+ )
class Meta(BaseTable.Meta):
model = Secret
- fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash')
+ fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash', 'tags')
default_columns = ('pk', 'device', 'role', 'name', 'last_updated')
diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html
index 8edc83f9c..638220f5a 100644
--- a/netbox/templates/utilities/templatetags/tag.html
+++ b/netbox/templates/utilities/templatetags/tag.html
@@ -1,5 +1,3 @@
{% load helpers %}
-{% if url_name %}{% endif %}
-{{ tag }}
-{% if url_name %}{% endif %}
+{% if url_name %}{% endif %}{{ tag }}{% if url_name %}{% endif %}
diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py
index 72fb98e80..147a20707 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, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
from .models import Tenant, TenantGroup
MPTT_LINK = """
@@ -63,8 +63,11 @@ class TenantGroupTable(BaseTable):
class TenantTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
+ tags = TagColumn(
+ url_name='tenancy:tenant_list'
+ )
class Meta(BaseTable.Meta):
model = Tenant
- fields = ('pk', 'name', 'slug', 'group', 'description')
+ fields = ('pk', 'name', 'slug', 'group', 'description', 'tags')
default_columns = ('pk', 'name', 'group', 'description')
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py
index e5433e3f8..0702936b5 100644
--- a/netbox/utilities/tables.py
+++ b/netbox/utilities/tables.py
@@ -1,8 +1,9 @@
import django_tables2 as tables
from django.core.exceptions import FieldDoesNotExist
from django.db.models import ForeignKey
-from django_tables2.data import TableQuerysetData
+from django.db.models.fields.related import RelatedField
from django.utils.safestring import mark_safe
+from django_tables2.data import TableQuerysetData
class BaseTable(tables.Table):
@@ -57,7 +58,7 @@ class BaseTable(tables.Table):
field_path = column.accessor.split('.')
try:
model_field = model._meta.get_field(field_path[0])
- if isinstance(model_field, ForeignKey):
+ if isinstance(model_field, RelatedField):
prefetch_fields.append('__'.join(field_path))
except FieldDoesNotExist:
pass
@@ -121,3 +122,22 @@ class ColorColumn(tables.Column):
return mark_safe(
' '.format(value)
)
+
+
+class TagColumn(tables.TemplateColumn):
+ """
+ Display a list of tags assigned to the object.
+ """
+ template_code = """
+ {% for tag in value.all %}
+ {% include 'utilities/templatetags/tag.html' %}
+ {% empty %}
+ —
+ {% endfor %}
+ """
+
+ def __init__(self, url_name=None):
+ super().__init__(
+ template_code=self.template_code,
+ extra_context={'url_name': url_name}
+ )
diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py
index ddc5b8ff7..077add945 100644
--- a/netbox/virtualization/tables.py
+++ b/netbox/virtualization/tables.py
@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
CLUSTERTYPE_ACTIONS = """
@@ -108,10 +108,14 @@ class ClusterTable(BaseTable):
orderable=False,
verbose_name='VMs'
)
+ tags = TagColumn(
+ url_name='virtualization:cluster_list'
+ )
class Meta(BaseTable.Meta):
model = Cluster
- fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
+ fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count', 'tags')
+ default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
#
@@ -156,12 +160,15 @@ class VirtualMachineDetailTable(VirtualMachineTable):
verbose_name='IP Address',
template_code=VIRTUALMACHINE_PRIMARY_IP
)
+ tags = TagColumn(
+ url_name='virtualization:virtualmachine_list'
+ )
class Meta(BaseTable.Meta):
model = VirtualMachine
fields = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
- 'primary_ip6', 'primary_ip',
+ 'primary_ip6', 'primary_ip', 'tags',
)
default_columns = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',