diff --git a/docs/development/application-registry.md b/docs/development/application-registry.md index c7ac6ca46..c845cd5a7 100644 --- a/docs/development/application-registry.md +++ b/docs/development/application-registry.md @@ -53,6 +53,10 @@ This store maintains all registered items for plugins, such as navigation menus, A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it. +### `tables` + +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. + ### `views` 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()`. diff --git a/docs/plugins/development/tables.md b/docs/plugins/development/tables.md index f846139f0..9d57a9603 100644 --- a/docs/plugins/development/tables.md +++ b/docs/plugins/development/tables.md @@ -87,3 +87,28 @@ 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 django.utils.translation import gettext_lazy as _ + +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 151eb2f6b..ad8c18dcf 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -28,6 +28,7 @@ registry = Registry({ '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 cb53310cc..83dc3ae3c 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 django_tables2.data import TableQuerysetData 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 @@ class NetBoxTable(BaseTable): 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 000000000..0f1e823d7 --- /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 8713102c5..03a83b585 100644 --- a/netbox/netbox/tests/dummy_plugin/views.py +++ b/netbox/netbox/tests/dummy_plugin/views.py @@ -4,6 +4,8 @@ from django.views.generic import View 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 046436a86..40bf8b0ea 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -97,6 +97,16 @@ class PluginTest(TestCase): 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 489b90f10..654eb02be 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