Skip to content

Commit e767fec

Browse files
Closes #14173: Enable plugins to register columns on core tables (#14265)
* Closes #14173: Enable plugins to register columns on core tables * Support translation for column name * Document new registry store
1 parent e15647a commit e767fec

File tree

8 files changed

+81
-1
lines changed

8 files changed

+81
-1
lines changed

docs/development/application-registry.md

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ This store maintains all registered items for plugins, such as navigation menus,
5353

5454
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
5555

56+
### `tables`
57+
58+
A dictionary mapping table classes to lists of extra columns that have been registered by plugins using the `register_table_column()` utility function. Each column is defined as a tuple of name and column instance.
59+
5660
### `views`
5761

5862
A hierarchical mapping of registered views for each model. Mappings are added using the `register_model_view()` decorator, and URLs paths can be generated from these using `get_model_urls()`.

docs/plugins/development/tables.md

+25
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,28 @@ The table column classes listed below are supported for use in plugins. These cl
8787
options:
8888
members:
8989
- __init__
90+
91+
## Extending Core Tables
92+
93+
!!! info "This feature was introduced in NetBox v3.7."
94+
95+
Plugins can register their own custom columns on core tables using the `register_table_column()` utility function. This allows a plugin to attach additional information, such as relationships to its own models, to built-in object lists.
96+
97+
```python
98+
import django_tables2
99+
from django.utils.translation import gettext_lazy as _
100+
101+
from dcim.tables import SiteTable
102+
from utilities.tables import register_table_column
103+
104+
mycol = django_tables2.Column(
105+
verbose_name=_('My Column'),
106+
accessor=django_tables2.A('description')
107+
)
108+
109+
register_table_column(mycol, 'foo', SiteTable)
110+
```
111+
112+
You'll typically want to define an accessor identifying the desired model field or relationship when defining a custom column. See the [django-tables2 documentation](https://django-tables2.readthedocs.io/) for more information on creating custom columns.
113+
114+
::: utilities.tables.register_table_column

netbox/netbox/registry.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __delitem__(self, key):
2828
'models': collections.defaultdict(set),
2929
'plugins': dict(),
3030
'search': dict(),
31+
'tables': collections.defaultdict(dict),
3132
'views': collections.defaultdict(dict),
3233
'widgets': dict(),
3334
})

netbox/netbox/tables/tables.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from copy import deepcopy
2+
13
import django_tables2 as tables
24
from django.contrib.auth.models import AnonymousUser
35
from django.contrib.contenttypes.fields import GenericForeignKey
@@ -12,6 +14,7 @@
1214

1315
from extras.models import CustomField, CustomLink
1416
from extras.choices import CustomFieldVisibilityChoices
17+
from netbox.registry import registry
1518
from netbox.tables import columns
1619
from utilities.paginator import EnhancedPaginator, get_paginate_count
1720
from utilities.utils import get_viewname, highlight_string, title
@@ -191,12 +194,17 @@ def __init__(self, *args, extra_columns=None, **kwargs):
191194
if extra_columns is None:
192195
extra_columns = []
193196

197+
if registered_columns := registry['tables'].get(self.__class__):
198+
extra_columns.extend([
199+
# Create a copy to avoid modifying the original Column
200+
(name, deepcopy(column)) for name, column in registered_columns.items()
201+
])
202+
194203
# Add custom field & custom link columns
195204
content_type = ContentType.objects.get_for_model(self._meta.model)
196205
custom_fields = CustomField.objects.filter(
197206
content_types=content_type
198207
).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN)
199-
200208
extra_columns.extend([
201209
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
202210
])
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import django_tables2 as tables
2+
3+
from dcim.tables import SiteTable
4+
from utilities.tables import register_table_column
5+
6+
mycol = tables.Column(
7+
verbose_name='My column',
8+
accessor=tables.A('description')
9+
)
10+
11+
register_table_column(mycol, 'foo', SiteTable)

netbox/netbox/tests/dummy_plugin/views.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from dcim.models import Site
55
from utilities.views import register_model_view
66
from .models import DummyModel
7+
# Trigger registration of custom column
8+
from .tables import mycol
79

810

911
class DummyModelsView(View):

netbox/netbox/tests/test_plugins.py

+10
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ def test_template_extensions(self):
9797

9898
self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
9999

100+
def test_registered_columns(self):
101+
"""
102+
Check that a plugin can register a custom column on a core model table.
103+
"""
104+
from dcim.models import Site
105+
from dcim.tables import SiteTable
106+
107+
table = SiteTable(Site.objects.all())
108+
self.assertIn('foo', table.columns.names())
109+
100110
def test_user_preferences(self):
101111
"""
102112
Check that plugin UserPreferences are registered.

netbox/utilities/tables.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from netbox.registry import registry
2+
13
__all__ = (
24
'get_table_ordering',
35
'linkify_phone',
6+
'register_table_column'
47
)
58

69

@@ -26,3 +29,19 @@ def linkify_phone(value):
2629
if value is None:
2730
return None
2831
return f"tel:{value}"
32+
33+
34+
def register_table_column(column, name, *tables):
35+
"""
36+
Register a custom column for use on one or more tables.
37+
38+
Args:
39+
column: The column instance to register
40+
name: The name of the table column
41+
tables: One or more table classes
42+
"""
43+
for table in tables:
44+
reg = registry['tables'][table]
45+
if name in reg:
46+
raise ValueError(f"A column named {name} is already defined for table {table.__name__}")
47+
reg[name] = column

0 commit comments

Comments
 (0)