diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md
index 140edba09..3d4c4cb25 100644
--- a/docs/release-notes/version-3.0.md
+++ b/docs/release-notes/version-3.0.md
@@ -4,6 +4,7 @@
### Bug Fixes
+* [#6895](https://github.com/netbox-community/netbox/issues/6895) - Remove errant markup for null values in CSV export
* [#7373](https://github.com/netbox-community/netbox/issues/7373) - Fix flashing when server, client, and browser color-mode preferences are mismatched
---
diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py
index 9509ec2bc..14cf34505 100644
--- a/netbox/dcim/tables/cables.py
+++ b/netbox/dcim/tables/cables.py
@@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import Cable
-from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
__all__ = (
@@ -45,7 +45,7 @@ class CableTable(BaseTable):
verbose_name='Termination B'
)
status = ChoiceFieldColumn()
- length = tables.TemplateColumn(
+ length = TemplateColumn(
template_code=CABLE_LENGTH,
order_by='_abs_length'
)
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 306b29b09..c22e673b7 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -9,7 +9,7 @@ from dcim.models import (
from tenancy.tables import TenantColumn
from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
- MarkdownColumn, TagColumn, ToggleColumn,
+ MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
)
from .template_code import (
CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS,
@@ -258,7 +258,7 @@ class CableTerminationTable(BaseTable):
orderable=False,
verbose_name='Cable Color'
)
- cable_peer = tables.TemplateColumn(
+ cable_peer = TemplateColumn(
accessor='_cable_peer',
template_code=CABLETERMINATION,
orderable=False,
@@ -268,7 +268,7 @@ class CableTerminationTable(BaseTable):
class PathEndpointTable(CableTerminationTable):
- connection = tables.TemplateColumn(
+ connection = TemplateColumn(
accessor='_path.last_node',
template_code=CABLETERMINATION,
verbose_name='Connection',
@@ -470,7 +470,7 @@ class BaseInterfaceTable(BaseTable):
verbose_name='IP Addresses'
)
untagged_vlan = tables.Column(linkify=True)
- tagged_vlans = tables.TemplateColumn(
+ tagged_vlans = TemplateColumn(
template_code=INTERFACE_TAGGED_VLANS,
orderable=False,
verbose_name='Tagged VLANs'
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 65a30ac61..2f359e1b9 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -5,13 +5,11 @@ CABLETERMINATION = """
{% endif %}
{{ value }}
-{% else %}
- —
{% endif %}
"""
CABLE_LENGTH = """
-{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
+{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% endif %}
"""
CABLE_TERMINATION_PARENT = """
@@ -63,8 +61,6 @@ INTERFACE_TAGGED_VLANS = """
{% endfor %}
{% elif record.mode == 'tagged-all' %}
All
-{% else %}
- —
{% endif %}
"""
diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py
index 57adbb1b8..485e4a123 100644
--- a/netbox/ipam/tables/ip.py
+++ b/netbox/ipam/tables/ip.py
@@ -39,15 +39,7 @@ PREFIXFLAT_LINK = """
{% if record.pk %}
{{ record.prefix }}
{% else %}
- —
-{% endif %}
-"""
-
-PREFIX_ROLE_LINK = """
-{% if record.role %}
- {{ record.role }}
-{% else %}
- —
+ {{ record.prefix }}
{% endif %}
"""
@@ -218,8 +210,8 @@ class PrefixTable(BaseTable):
linkify=True,
verbose_name='VLAN'
)
- role = tables.TemplateColumn(
- template_code=PREFIX_ROLE_LINK
+ role = tables.Column(
+ linkify=True
)
is_pool = BooleanColumn(
verbose_name='Pool'
@@ -264,8 +256,8 @@ class IPRangeTable(BaseTable):
status = ChoiceFieldColumn(
default=AVAILABLE_LABEL
)
- role = tables.TemplateColumn(
- template_code=PREFIX_ROLE_LINK
+ role = tables.Column(
+ linkify=True
)
tenant = TenantColumn()
diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py
index 4219a8a52..fd1e92be8 100644
--- a/netbox/ipam/tables/vlans.py
+++ b/netbox/ipam/tables/vlans.py
@@ -6,7 +6,7 @@ from dcim.models import Interface
from tenancy.tables import TenantColumn
from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
- ToggleColumn,
+ TemplateColumn, ToggleColumn,
)
from virtualization.models import VMInterface
from ipam.models import *
@@ -35,19 +35,9 @@ VLAN_LINK = """
VLAN_PREFIXES = """
{% for prefix in record.prefixes.all %}
{{ prefix }}{% if not forloop.last %}
{% endif %}
-{% empty %}
- —
{% endfor %}
"""
-VLAN_ROLE_LINK = """
-{% if record.role %}
- {{ record.role }}
-{% else %}
- —
-{% endif %}
-"""
-
VLANGROUP_ADD_VLAN = """
{% with next_vid=record.get_next_available_vid %}
{% if next_vid and perms.ipam.add_vlan %}
@@ -115,10 +105,10 @@ class VLANTable(BaseTable):
status = ChoiceFieldColumn(
default=AVAILABLE_LABEL
)
- role = tables.TemplateColumn(
- template_code=VLAN_ROLE_LINK
+ role = tables.Column(
+ linkify=True
)
- prefixes = tables.TemplateColumn(
+ prefixes = TemplateColumn(
template_code=VLAN_PREFIXES,
orderable=False,
verbose_name='Prefixes'
@@ -190,8 +180,8 @@ class InterfaceVLANTable(BaseTable):
)
tenant = TenantColumn()
status = ChoiceFieldColumn()
- role = tables.TemplateColumn(
- template_code=VLAN_ROLE_LINK
+ role = tables.Column(
+ linkify=True
)
class Meta(BaseTable.Meta):
diff --git a/netbox/ipam/tables/vrfs.py b/netbox/ipam/tables/vrfs.py
index bea2a6b1f..3a351a856 100644
--- a/netbox/ipam/tables/vrfs.py
+++ b/netbox/ipam/tables/vrfs.py
@@ -1,7 +1,7 @@
import django_tables2 as tables
from tenancy.tables import TenantColumn
-from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, BooleanColumn, TagColumn, TemplateColumn, ToggleColumn
from ipam.models import *
__all__ = (
@@ -11,9 +11,7 @@ __all__ = (
VRF_TARGETS = """
{% for rt in value.all %}
- {{ rt }}{% if not forloop.last %}
{% endif %}
-{% empty %}
- —
+ {{ rt }}{% if not forloop.last %}
{% endif %}
{% endfor %}
"""
@@ -34,11 +32,11 @@ class VRFTable(BaseTable):
enforce_unique = BooleanColumn(
verbose_name='Unique'
)
- import_targets = tables.TemplateColumn(
+ import_targets = TemplateColumn(
template_code=VRF_TARGETS,
orderable=False
)
- export_targets = tables.TemplateColumn(
+ export_targets = TemplateColumn(
template_code=VRF_TARGETS,
orderable=False
)
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py
index 0a8e8c3ba..446438b5c 100644
--- a/netbox/utilities/tables.py
+++ b/netbox/utilities/tables.py
@@ -157,6 +157,25 @@ class BooleanColumn(tables.Column):
return str(value)
+class TemplateColumn(tables.TemplateColumn):
+ """
+ Overrides the stock TemplateColumn to render a placeholder if the returned value is an empty string.
+ """
+ PLACEHOLDER = mark_safe('—')
+
+ def render(self, *args, **kwargs):
+ ret = super().render(*args, **kwargs)
+ if not ret.strip():
+ return self.PLACEHOLDER
+ return ret
+
+ def value(self, **kwargs):
+ ret = super().value(**kwargs)
+ if ret == self.PLACEHOLDER:
+ return ''
+ return ret
+
+
class ButtonsColumn(tables.TemplateColumn):
"""
Render edit, delete, and changelog buttons for an object.