diff --git a/netbox/templates/users/panels/actions.html b/netbox/templates/users/panels/actions.html
index ffd7d2d95..d6ce07856 100644
--- a/netbox/templates/users/panels/actions.html
+++ b/netbox/templates/users/panels/actions.html
@@ -1,5 +1,5 @@
{% extends "ui/panels/_base.html" %}
-{% load helpers %}
+{% load helpers i18n %}
{% block panel_content %}
@@ -9,18 +9,24 @@
| {% checkmark enabled %} |
{% endfor %}
- {% for action, enabled, models in registered_actions %}
+ {% for action in registered_actions %}
- | {{ action }} |
+ {{ action.help_text|default:action.name }} |
- {% checkmark enabled %}
- {% if models %}
- {{ models|join:", " }}
+ {% checkmark action.enabled %}
+ {% if action.models %}
+ {{ action.models|join:", "|title }}
{% endif %}
|
{% endfor %}
+ {% if additional_actions %}
+
+ | {% trans "Additional actions" %} |
+ {{ additional_actions|join:", " }} |
+
+ {% endif %}
{% endblock panel_content %}
diff --git a/netbox/users/models/permissions.py b/netbox/users/models/permissions.py
index 74b8f589e..80fedd9a1 100644
--- a/netbox/users/models/permissions.py
+++ b/netbox/users/models/permissions.py
@@ -1,3 +1,4 @@
+from django.apps import apps
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.urls import reverse
@@ -86,20 +87,52 @@ class ObjectPermission(CloningMixin, models.Model):
def get_registered_actions(self):
"""
- Return a list of (action_name, is_enabled, model_keys) tuples for all
- registered actions, indicating which are enabled on this permission.
+ Return a list of dicts for all registered actions:
+ name: The action identifier
+ help_text: Human-friendly description (first registration wins)
+ enabled: Whether this action is enabled on this permission
+ models: Sorted list of human-friendly model verbose names
"""
enabled_actions = set(self.actions) - set(RESERVED_ACTIONS)
+ action_info = {}
action_models = {}
for model_key, model_actions in registry['model_actions'].items():
+ app_label, model_name = model_key.split('.')
+ try:
+ verbose_name = str(apps.get_model(app_label, model_name)._meta.verbose_name)
+ except LookupError:
+ verbose_name = model_key
for action in model_actions:
- action_models.setdefault(action.name, []).append(model_key)
+ # First registration's help_text wins for shared action names
+ if action.name not in action_info:
+ action_info[action.name] = action
+ action_models.setdefault(action.name, []).append(verbose_name)
return [
- (name, name in enabled_actions, sorted(action_models[name]))
+ {
+ 'name': name,
+ 'help_text': action_info[name].help_text,
+ 'enabled': name in enabled_actions,
+ 'models': sorted(action_models[name]),
+ }
for name in sorted(action_models)
]
+ def get_additional_actions(self):
+ """
+ Return a sorted list of actions that are neither CRUD nor registered.
+ These are manually-entered actions from the "Additional actions" field.
+ """
+ registered_names = set()
+ for model_actions in registry['model_actions'].values():
+ for action in model_actions:
+ registered_names.add(action.name)
+
+ return sorted(
+ a for a in self.actions
+ if a not in RESERVED_ACTIONS and a not in registered_names
+ )
+
def get_absolute_url(self):
return reverse('users:objectpermission', args=[self.pk])
diff --git a/netbox/users/ui/panels.py b/netbox/users/ui/panels.py
index 7c3b9ff71..35a40b88b 100644
--- a/netbox/users/ui/panels.py
+++ b/netbox/users/ui/panels.py
@@ -63,6 +63,7 @@ class ObjectPermissionActionsPanel(panels.ObjectPanel):
**super().get_context(context),
'crud_actions': crud_actions,
'registered_actions': obj.get_registered_actions(),
+ 'additional_actions': obj.get_additional_actions(),
}
diff --git a/netbox/utilities/tests/test_permissions.py b/netbox/utilities/tests/test_permissions.py
index 34e8450b5..dd4dd7cad 100644
--- a/netbox/utilities/tests/test_permissions.py
+++ b/netbox/utilities/tests/test_permissions.py
@@ -167,11 +167,11 @@ class ObjectPermissionFormTest(TestCase):
registered = permission.get_registered_actions()
self.assertEqual(len(registered), 1)
- action_name, is_enabled, model_keys = registered[0]
- self.assertEqual(action_name, 'render_config')
- self.assertTrue(is_enabled)
- self.assertIn('dcim.device', model_keys)
- self.assertIn('virtualization.virtualmachine', model_keys)
+ action = registered[0]
+ self.assertEqual(action['name'], 'render_config')
+ self.assertEqual(action['help_text'], '')
+ self.assertTrue(action['enabled'])
+ self.assertEqual(action['models'], ['device', 'virtual machine'])
permission.delete()