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',