Tables#
Tables live under netbox_aci_plugin/tables/<domain>/<model>.py. Every
table extends netbox.tables.NetBoxTable and uses
netbox.tables.columns plus django_tables2.Column for column
declarations.
Column conventions#
accessor over render_*#
For FK traversal (e.g. showing the grandparent fabric on a child
table), prefer accessor="parent__grandparent" over a custom
render_<column>() method:
aci_fabric = tables.Column(
verbose_name=_("Fabric"),
accessor="aci_tenant__aci_fabric",
linkify=True,
)
accessor is declarative, works with sorting and CSV export
automatically, and stays in one place. Reach for render_* only when
the cell needs computed HTML that accessor can't express.
Linkify FKs and identifier columns#
Use linkify=True on the name, name_alias, and any FK column.
NetBox's table machinery routes the link to the related object's
detail page automatically.
Column headers#
Every rendered name column, and every FK or GFK column that points at
another object, passes an explicit short verbose_name: the model name
without the ACI prefix (Fabric, VLAN Pool, External EPG,
L3Out). Keep the ACI prefix only where a NetBox core model of the
same name appears in the same view, which today means columns pointing
at ACI Tenant and ACI VRF (ACI Tenant vs NB Tenant, ACI VRF vs NB
VRF). The polymorphic GFK columns that can resolve to either an
Endpoint Group or a uSeg Endpoint Group use the shared umbrella
header EPG (and EPG Type for the paired content-type column) rather
than either model's own short name. NetBox-side nb_tenant and other
nb_* columns keep their own "NetBox X" labelling, a separate concern
from this rule. A guard test enforces the convention across every
table.
Column-type catalog#
Use these column classes consistently:
tables.Column: plain string and FK fields. Pair it withaccessorfor traversal andlinkify=Truefor clickability.columns.BooleanColumn: every_enabled/_disabledboolean field. Always pass a shortverbose_name; the model's verbose name is too long for a table header.columns.ChoiceFieldColumn: any field backed by aChoiceSet. It renders the badge with the color fromget_<field>_color().columns.ArrayColumn: list-typed fields such asdhcp_labels.columns.TagColumn: the model'stagsManyToMany. This is the second-to-last column (immediately beforecomments).columns.MarkdownColumn: the model'scommentsfield. This is the standard last column.columns.TemplateColumn: cells that need composed HTML, such as nested links or conditional badges. Pair it with a module-leveltemplate_code = """..."""literal.
ArrayColumn for ArrayField data#
For model fields backed by Django's ArrayField (e.g.
security_domains, dhcp_labels), use columns.ArrayColumn()
instead of tables.Column() with a custom render_* method.
ArrayColumn handles comma-separated rendering automatically.
BooleanColumn verbose-name shortening#
Model boolean fields use verbose names like
_("preferred group member enabled"): descriptive but too long for a
table header. Inside the column declaration, drop the enabled suffix
and abbreviate where natural:
arp_flooding_enabled = columns.BooleanColumn(verbose_name=_("ARP flooding"))
clear_remote_mac_enabled = columns.BooleanColumn(verbose_name=_("Clear remote MAC"))
ep_move_detection_enabled = columns.BooleanColumn(verbose_name=_("EP move detect"))
ip_data_plane_learning_enabled = columns.BooleanColumn(verbose_name=_("DP learning"))
See tables/tenant/bridge_domains.py for the full set of standard
abbreviations.
TemplateColumn patterns#
For inlined child renderings (e.g. "show all subnets of this BD in one
cell"), define a template_code literal near the top of the table
file and reference it from the column:
BRIDGEDOMAIN_SUBNETS = """
{% for bd_subnet in value.all %}
<a href="../{% url 'plugins:netbox_aci_plugin:acibridgedomainsubnet'
pk=bd_subnet.pk %}">
{{ bd_subnet.gateway_ip_address }}
</a>{% if not forloop.last %}<br />{% endif %}
{% endfor %}
"""
class ACIBridgeDomainTable(NetBoxTable):
aci_bridge_domain_subnets = columns.TemplateColumn(
verbose_name=_("BD Subnets"),
orderable=False,
template_code=BRIDGEDOMAIN_SUBNETS,
)
orderable=False is required when the template iterates a queryset;
there's no scalar to order by.
Meta.fields and Meta.default_columns#
Every table declares both tuples on Meta:
fields: every column the table can render. Includes hidden-by- default columns that users can opt into via the column picker.pkandidare always first;tagsandcommentsare always last.default_columns: the columns visible without user customization. Shorter list; what shows up in a fresh install. Must include"name_alias"immediately after"name"on every model that has aname_aliasfield.- Derived accessor-traversal columns (e.g.
aci_fabricdeclared viaaccessor="aci_tenant__aci_fabric") belong infieldstoo - they're columns like any other, and the column picker should be able to toggle them even though they resolve through a parent FK rather than a field on the model itself.
class Meta(NetBoxTable.Meta):
model = ACIBridgeDomain
fields: tuple = (
"pk",
"id",
"name",
"name_alias",
"description",
"aci_tenant",
"aci_vrf",
"nb_tenant",
"advertise_host_routes_enabled",
# ... all other columns
"owner",
"tags",
"comments",
)
default_columns: tuple = (
"name",
"name_alias",
"aci_tenant",
"aci_vrf",
"nb_tenant",
"description",
"unicast_routing_enabled",
"tags",
)
Both tuples annotated : tuple.
Reduced tables#
A reduced table is a trimmed variant of a full table, used where a detail page embeds a child list and the full column set would be too wide for the card.
Reach for one only when no list view can produce the rows. Since the
declarative UI port, an embedded child list that a filter can express is
rendered by ObjectsTablePanel, which reuses the child's own list table
and trims it with exclude_columns (see UI - Embedded child
tables). That path gets pagination,
sorting, column preferences and permission scoping for free, so it is
the default.
What is left for a reduced table is the case ObjectsTablePanel cannot
serve: a computed set of rows with no corresponding list filter. The
plugin has exactly one, ACINodeReducedTable, which backs the resolved
node cards on the Leaf Selector, Leaf Node Block and Leaf Switch Profile
pages. Those rows come from range membership, which no filter expresses,
so the view builds the table in get_extra_context() and a
ContextTablePanel renders it.
class ACINodeReducedTable(NetBoxTable):
"""Reduced NetBox table for the ACI Node model."""
name = tables.Column(
verbose_name=_("Node"),
linkify=True,
)
node_id = tables.Column(
verbose_name=_("Node ID"),
linkify=True,
)
role = columns.ChoiceFieldColumn(
verbose_name=_("Role"),
)
class Meta(NetBoxTable.Meta):
model = ACINode
fields: tuple = ("pk", "id", "name", "node_id", "role")
default_columns: tuple = ("name", "node_id", "role")
Drop the parent FK column, since the reader is already on the parent's page, and keep only the columns that carry meaning in that context.
Naming: append Reduced before Table. Place the reduced table in the
same file as the full table.
Table column kwarg ordering#
Pass kwargs to django_tables2.Column in this order. Skip any that
aren't needed; don't reorder:
verbose_name
accessor
default
visible
orderable
attrs
order_by
empty_values
localize
footer
exclude_from_export
linkify
initial_sort_descending
Future adoption
NetBox ships LinkedCountColumn (clickable badge counts linking to
filtered lists), which could replace hand-rolled parent/child count
displays in current tables.