Просмотр исходного кода

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
Jeremy Stretch 2 лет назад
Родитель
Сommit
e767fec5cc

+ 4 - 0
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()`.

+ 25 - 0
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

+ 1 - 0
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(),
 })

+ 9 - 1
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
         ])

+ 11 - 0
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)

+ 2 - 0
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):

+ 10 - 0
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.

+ 19 - 0
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