Просмотр исходного кода

Closes #3064: Include tags in object lists as a toggleable table column

Jeremy Stretch 5 лет назад
Родитель
Сommit
a4dbd2dae5

+ 1 - 0
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
 * [#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
 * [#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
 * [#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
 * [#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
 * [#4502](https://github.com/netbox-community/netbox/issues/4502) - Enable configuration of proxies for outbound HTTP requests

+ 27 - 8
netbox/circuits/tables.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from tenancy.tables import COL_TENANT
 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
 from .models import Circuit, CircuitType, Provider
 
 
 CIRCUITTYPE_ACTIONS = """
 CIRCUITTYPE_ACTIONS = """
@@ -31,10 +31,15 @@ class ProviderTable(BaseTable):
         accessor=Accessor('count_circuits'),
         accessor=Accessor('count_circuits'),
         verbose_name='Circuits'
         verbose_name='Circuits'
     )
     )
+    tags = TagColumn(
+        url_name='circuits:provider_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Provider
         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')
         default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
 
 
 
 
@@ -45,7 +50,9 @@ class ProviderTable(BaseTable):
 class CircuitTypeTable(BaseTable):
 class CircuitTypeTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
-    circuit_count = tables.Column(verbose_name='Circuits')
+    circuit_count = tables.Column(
+        verbose_name='Circuits'
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=CIRCUITTYPE_ACTIONS,
         template_code=CIRCUITTYPE_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -64,21 +71,33 @@ class CircuitTypeTable(BaseTable):
 
 
 class CircuitTable(BaseTable):
 class CircuitTable(BaseTable):
     pk = ToggleColumn()
     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(
     a_side = tables.Column(
         verbose_name='A Side'
         verbose_name='A Side'
     )
     )
     z_side = tables.Column(
     z_side = tables.Column(
         verbose_name='Z Side'
         verbose_name='Z Side'
     )
     )
+    tags = TagColumn(
+        url_name='circuits:circuit_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Circuit
         model = Circuit
         fields = (
         fields = (
             'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate',
             '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')
         default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')

+ 28 - 8
netbox/dcim/tables.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from tenancy.tables import COL_TENANT
 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 (
 from .models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -242,13 +242,16 @@ class SiteTable(BaseTable):
     tenant = tables.TemplateColumn(
     tenant = tables.TemplateColumn(
         template_code=COL_TENANT
         template_code=COL_TENANT
     )
     )
+    tags = TagColumn(
+        url_name='dcim:site_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Site
         model = Site
         fields = (
         fields = (
             'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
             'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description',
             'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
             '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')
         default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description')
 
 
@@ -354,11 +357,14 @@ class RackDetailTable(RackTable):
         orderable=False,
         orderable=False,
         verbose_name='Power'
         verbose_name='Power'
     )
     )
+    tags = TagColumn(
+        url_name='dcim:rack_list'
+    )
 
 
     class Meta(RackTable.Meta):
     class Meta(RackTable.Meta):
         fields = (
         fields = (
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
             '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 = (
         default_columns = (
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
@@ -450,17 +456,22 @@ class DeviceTypeTable(BaseTable):
         args=[Accessor('pk')],
         args=[Accessor('pk')],
         verbose_name='Device Type'
         verbose_name='Device Type'
     )
     )
-    is_full_depth = BooleanColumn(verbose_name='Full Depth')
+    is_full_depth = BooleanColumn(
+        verbose_name='Full Depth'
+    )
     instance_count = tables.TemplateColumn(
     instance_count = tables.TemplateColumn(
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         verbose_name='Instances'
         verbose_name='Instances'
     )
     )
+    tags = TagColumn(
+        url_name='dcim:devicetype_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = DeviceType
         model = DeviceType
         fields = (
         fields = (
             'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
             'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
-            'instance_count',
+            'instance_count', 'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
             'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@@ -834,13 +845,16 @@ class DeviceTable(BaseTable):
     vc_priority = tables.Column(
     vc_priority = tables.Column(
         verbose_name='VC Priority'
         verbose_name='VC Priority'
     )
     )
+    tags = TagColumn(
+        url_name='dcim:device_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Device
         model = Device
         fields = (
         fields = (
             'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site',
             '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',
             'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis',
-            'vc_position', 'vc_priority',
+            'vc_position', 'vc_priority', 'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
             'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip',
@@ -1206,10 +1220,13 @@ class VirtualChassisTable(BaseTable):
     member_count = tables.Column(
     member_count = tables.Column(
         verbose_name='Members'
         verbose_name='Members'
     )
     )
+    tags = TagColumn(
+        url_name='dcim:virtualchassis_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VirtualChassis
         model = VirtualChassis
-        fields = ('pk', 'name', 'domain', 'member_count')
+        fields = ('pk', 'name', 'domain', 'member_count', 'tags')
         default_columns = ('pk', 'name', 'domain', 'member_count')
         default_columns = ('pk', 'name', 'domain', 'member_count')
 
 
 
 
@@ -1262,12 +1279,15 @@ class PowerFeedTable(BaseTable):
     available_power = tables.Column(
     available_power = tables.Column(
         verbose_name='Available power (VA)'
         verbose_name='Available power (VA)'
     )
     )
+    tags = TagColumn(
+        url_name='dcim:powerfeed_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerFeed
         model = PowerFeed
         fields = (
         fields = (
             'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
             'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
-            'max_utilization', 'available_power',
+            'max_utilization', 'available_power', 'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
             'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',

+ 27 - 6
netbox/ipam/tables.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from tenancy.tables import COL_TENANT
 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
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 RIR_UTILIZATION = """
 RIR_UTILIZATION = """
@@ -199,10 +199,13 @@ class VRFTable(BaseTable):
     enforce_unique = BooleanColumn(
     enforce_unique = BooleanColumn(
         verbose_name='Unique'
         verbose_name='Unique'
     )
     )
+    tags = TagColumn(
+        url_name='ipam:vrf_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VRF
         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')
         default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
 
 
 
 
@@ -300,9 +303,13 @@ class AggregateDetailTable(AggregateTable):
         template_code=UTILIZATION_GRAPH,
         template_code=UTILIZATION_GRAPH,
         orderable=False
         orderable=False
     )
     )
+    tags = TagColumn(
+        url_name='ipam:aggregate_list'
+    )
 
 
     class Meta(AggregateTable.Meta):
     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(
     tenant = tables.TemplateColumn(
         template_code=COL_TENANT
         template_code=COL_TENANT
     )
     )
+    tags = TagColumn(
+        url_name='ipam:prefix_list'
+    )
 
 
     class Meta(PrefixTable.Meta):
     class Meta(PrefixTable.Meta):
         fields = (
         fields = (
             'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
             'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
+            'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
             'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
@@ -446,11 +457,14 @@ class IPAddressDetailTable(IPAddressTable):
     tenant = tables.TemplateColumn(
     tenant = tables.TemplateColumn(
         template_code=COL_TENANT
         template_code=COL_TENANT
     )
     )
+    tags = TagColumn(
+        url_name='ipam:ipaddress_list'
+    )
 
 
     class Meta(IPAddressTable.Meta):
     class Meta(IPAddressTable.Meta):
         fields = (
         fields = (
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
-            'description',
+            'description', 'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
@@ -573,9 +587,13 @@ class VLANDetailTable(VLANTable):
     tenant = tables.TemplateColumn(
     tenant = tables.TemplateColumn(
         template_code=COL_TENANT
         template_code=COL_TENANT
     )
     )
+    tags = TagColumn(
+        url_name='ipam:vlan_list'
+    )
 
 
     class Meta(VLANTable.Meta):
     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):
 class VLANMemberTable(BaseTable):
@@ -647,8 +665,11 @@ class ServiceTable(BaseTable):
         viewname='ipam:service',
         viewname='ipam:service',
         args=[Accessor('pk')]
         args=[Accessor('pk')]
     )
     )
+    tags = TagColumn(
+        url_name='ipam:service_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Service
         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')
         default_columns = ('pk', 'name', 'parent', 'protocol', 'port', 'description')

+ 5 - 2
netbox/secrets/tables.py

@@ -1,6 +1,6 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
-from utilities.tables import BaseTable, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
 from .models import SecretRole, Secret
 from .models import SecretRole, Secret
 
 
 SECRETROLE_ACTIONS = """
 SECRETROLE_ACTIONS = """
@@ -42,8 +42,11 @@ class SecretRoleTable(BaseTable):
 class SecretTable(BaseTable):
 class SecretTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     device = tables.LinkColumn()
     device = tables.LinkColumn()
+    tags = TagColumn(
+        url_name='secrets:secret_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Secret
         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')
         default_columns = ('pk', 'device', 'role', 'name', 'last_updated')

+ 1 - 3
netbox/templates/utilities/templatetags/tag.html

@@ -1,5 +1,3 @@
 {% load helpers %}
 {% load helpers %}
 
 
-{% if url_name %}<a href="{% url url_name %}?tag={{ tag.slug }}">{% endif %}
-<span class="label label-default" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span>
-{% if url_name %}</a>{% endif %}
+{% if url_name %}<a href="{% url url_name %}?tag={{ tag.slug }}">{% endif %}<span class="label label-default" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span>{% if url_name %}</a>{% endif %}

+ 5 - 2
netbox/tenancy/tables.py

@@ -1,6 +1,6 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
-from utilities.tables import BaseTable, ToggleColumn
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
 MPTT_LINK = """
 MPTT_LINK = """
@@ -63,8 +63,11 @@ class TenantGroupTable(BaseTable):
 class TenantTable(BaseTable):
 class TenantTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
+    tags = TagColumn(
+        url_name='tenancy:tenant_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Tenant
         model = Tenant
-        fields = ('pk', 'name', 'slug', 'group', 'description')
+        fields = ('pk', 'name', 'slug', 'group', 'description', 'tags')
         default_columns = ('pk', 'name', 'group', 'description')
         default_columns = ('pk', 'name', 'group', 'description')

+ 22 - 2
netbox/utilities/tables.py

@@ -1,8 +1,9 @@
 import django_tables2 as tables
 import django_tables2 as tables
 from django.core.exceptions import FieldDoesNotExist
 from django.core.exceptions import FieldDoesNotExist
 from django.db.models import ForeignKey
 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.utils.safestring import mark_safe
+from django_tables2.data import TableQuerysetData
 
 
 
 
 class BaseTable(tables.Table):
 class BaseTable(tables.Table):
@@ -57,7 +58,7 @@ class BaseTable(tables.Table):
                     field_path = column.accessor.split('.')
                     field_path = column.accessor.split('.')
                     try:
                     try:
                         model_field = model._meta.get_field(field_path[0])
                         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))
                             prefetch_fields.append('__'.join(field_path))
                     except FieldDoesNotExist:
                     except FieldDoesNotExist:
                         pass
                         pass
@@ -121,3 +122,22 @@ class ColorColumn(tables.Column):
         return mark_safe(
         return mark_safe(
             '<span class="label color-block" style="background-color: #{}">&nbsp;</span>'.format(value)
             '<span class="label color-block" style="background-color: #{}">&nbsp;</span>'.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 %}
+        <span class="text-muted">&mdash;</span>
+    {% endfor %}
+    """
+
+    def __init__(self, url_name=None):
+        super().__init__(
+            template_code=self.template_code,
+            extra_context={'url_name': url_name}
+        )

+ 10 - 3
netbox/virtualization/tables.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from tenancy.tables import COL_TENANT
 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
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 CLUSTERTYPE_ACTIONS = """
 CLUSTERTYPE_ACTIONS = """
@@ -108,10 +108,14 @@ class ClusterTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='VMs'
         verbose_name='VMs'
     )
     )
+    tags = TagColumn(
+        url_name='virtualization:cluster_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Cluster
         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',
         verbose_name='IP Address',
         template_code=VIRTUALMACHINE_PRIMARY_IP
         template_code=VIRTUALMACHINE_PRIMARY_IP
     )
     )
+    tags = TagColumn(
+        url_name='virtualization:virtualmachine_list'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VirtualMachine
         model = VirtualMachine
         fields = (
         fields = (
             'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
             'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4',
-            'primary_ip6', 'primary_ip',
+            'primary_ip6', 'primary_ip', 'tags',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
             'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',