diff --git a/docs/plugins/development/tables.md b/docs/plugins/development/tables.md index f846139f0b8..27128dc1938 100644 --- a/docs/plugins/development/tables.md +++ b/docs/plugins/development/tables.md @@ -87,3 +87,27 @@ The table column classes listed below are supported for use in plugins. These cl options: members: - __init__ + +## Extending Core Tables + +!!! info "This feature was introduced in NetBox v3.7." + +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. + +```python +import django_tables2 + +from dcim.tables import SiteTable +from utilities.tables import register_table_column + +mycol = django_tables2.Column( + verbose_name='My Column', + accessor=django_tables2.A('description') +) + +register_table_column(mycol, 'foo', SiteTable) +``` + +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. + +::: utilities.tables.register_table_column diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index 151eb2f6b4e..ad8c18dcfc3 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -28,6 +28,7 @@ def __delitem__(self, key): 'models': collections.defaultdict(set), 'plugins': dict(), 'search': dict(), + 'tables': collections.defaultdict(dict), 'views': collections.defaultdict(dict), 'widgets': dict(), }) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index cb53310ccfa..83dc3ae3cb2 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -1,3 +1,5 @@ +from copy import deepcopy + import django_tables2 as tables from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey @@ -12,6 +14,7 @@ from extras.models import CustomField, CustomLink from extras.choices import CustomFieldVisibilityChoices +from netbox.registry import registry from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.utils import get_viewname, highlight_string, title @@ -191,12 +194,17 @@ def __init__(self, *args, extra_columns=None, **kwargs): if extra_columns is None: extra_columns = [] + if registered_columns := registry['tables'].get(self.__class__): + extra_columns.extend([ + # Create a copy to avoid modifying the original Column + (name, deepcopy(column)) for name, column in registered_columns.items() + ]) + # Add custom field & custom link columns content_type = ContentType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter( content_types=content_type ).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN) - extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) diff --git a/netbox/netbox/tests/dummy_plugin/tables.py b/netbox/netbox/tests/dummy_plugin/tables.py new file mode 100644 index 00000000000..0f1e823d73f --- /dev/null +++ b/netbox/netbox/tests/dummy_plugin/tables.py @@ -0,0 +1,11 @@ +import django_tables2 as tables + +from dcim.tables import SiteTable +from utilities.tables import register_table_column + +mycol = tables.Column( + verbose_name='My column', + accessor=tables.A('description') +) + +register_table_column(mycol, 'foo', SiteTable) diff --git a/netbox/netbox/tests/dummy_plugin/views.py b/netbox/netbox/tests/dummy_plugin/views.py index 8713102c5ea..03a83b58595 100644 --- a/netbox/netbox/tests/dummy_plugin/views.py +++ b/netbox/netbox/tests/dummy_plugin/views.py @@ -4,6 +4,8 @@ from dcim.models import Site from utilities.views import register_model_view from .models import DummyModel +# Trigger registration of custom column +from .tables import mycol class DummyModelsView(View): diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index 046436a8689..40bf8b0ea76 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -97,6 +97,16 @@ def test_template_extensions(self): self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site']) + def test_registered_columns(self): + """ + Check that a plugin can register a custom column on a core model table. + """ + from dcim.models import Site + from dcim.tables import SiteTable + + table = SiteTable(Site.objects.all()) + self.assertIn('foo', table.columns.names()) + def test_user_preferences(self): """ Check that plugin UserPreferences are registered. diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 489b90f10ff..654eb02bea0 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -1,6 +1,9 @@ +from netbox.registry import registry + __all__ = ( 'get_table_ordering', 'linkify_phone', + 'register_table_column' ) @@ -26,3 +29,19 @@ def linkify_phone(value): if value is None: return None return f"tel:{value}" + + +def register_table_column(column, name, *tables): + """ + Register a custom column for use on one or more tables. + + Args: + column: The column instance to register + name: The name of the table column + tables: One or more table classes + """ + for table in tables: + reg = registry['tables'][table] + if name in reg: + raise ValueError(f"A column named {name} is already defined for table {table.__name__}") + reg[name] = column