Repository: netbox-community/netbox

Issue:
Make view name resolution plugin-safe
### Proposed Changes

There are several areas within NetBox where the automatic resolution of a view name from a model does not account for the possibility that the mode has been provided by a plugin. We can easily adjust these to support plugin models.

### Justification

This change is necessary to fully support plugins as part of the initiatives tracked under #8333.


Discussion:

Resolving patch:
diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -10,7 +10,7 @@
 from django_tables2.utils import Accessor
 
 from extras.choices import CustomFieldTypeChoices
-from utilities.utils import content_type_identifier, content_type_name
+from utilities.utils import content_type_identifier, content_type_name, resolve_namespace
 
 __all__ = (
     'ActionsColumn',
@@ -134,7 +134,7 @@ def render(self, record, table, **kwargs):
             return ''
 
         model = table.Meta.model
-        viewname_base = f'{model._meta.app_label}:{model._meta.model_name}'
+        viewname_base = f'{resolve_namespace(model)}:{model._meta.model_name}'
         request = getattr(table, 'context', {}).get('request')
         url_appendix = f'?return_url={request.path}' if request else ''
 
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py
--- a/netbox/utilities/templatetags/helpers.py
+++ b/netbox/utilities/templatetags/helpers.py
@@ -16,9 +16,10 @@
 from markdown import markdown
 
 from netbox.config import get_config
+from netbox.settings import PLUGINS
 from utilities.forms import get_selected_values, TableConfigForm
 from utilities.markdown import StrikethroughExtension
-from utilities.utils import foreground_color
+from utilities.utils import foreground_color, resolve_namespace
 
 register = template.Library()
 
@@ -115,7 +116,8 @@ def viewname(model, action):
     """
     Return the view name for the given model and action. Does not perform any validation.
     """
-    return f'{model._meta.app_label}:{model._meta.model_name}_{action}'
+    namespace = resolve_namespace(model)
+    return f'{namespace}:{model._meta.model_name}_{action}'
 
 
 @register.filter()
@@ -123,7 +125,8 @@ def validated_viewname(model, action):
     """
     Return the view name for the given model and action if valid, or None if invalid.
     """
-    viewname = f'{model._meta.app_label}:{model._meta.model_name}_{action}'
+    namespace = resolve_namespace(model)
+    viewname = f'{namespace}:{model._meta.model_name}_{action}'
     try:
         # Validate and return the view name. We don't return the actual URL yet because many of the templates
         # are written to pass a name to {% url %}.
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -13,10 +13,20 @@
 from mptt.models import MPTTModel
 
 from dcim.choices import CableLengthUnitChoices
+from extras.plugins import PluginConfig
 from extras.utils import is_taggable
 from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
 
 
+def resolve_namespace(instance):
+    """
+    Get the appropriate namepsace for the app based on whether it is a Plugin or base application
+    """
+    if isinstance(instance._meta.app_config, PluginConfig):
+        return f'plugins:{instance._meta.app_label}'
+    return f'{instance._meta.app_label}'
+
+
 def csv_format(data):
     """
     Encapsulate any data which contains a comma within double quotes.


Test patch: