diff --git a/docs/plugins/development/permissions.md b/docs/plugins/development/permissions.md index 246254a97..792632a59 100644 --- a/docs/plugins/development/permissions.md +++ b/docs/plugins/development/permissions.md @@ -6,7 +6,22 @@ For example, a plugin might define a "sync" action for a model that syncs data f ## Registering Model Actions -To register custom actions for a model, call `register_model_actions()` in your plugin's `ready()` method: +The preferred way to register custom actions is via Django's `Meta.permissions` on the model class. NetBox will automatically register these as model actions when the app is loaded: + +```python +from netbox.models import NetBoxModel + +class MyModel(NetBoxModel): + # ... + + class Meta: + permissions = [ + ('sync', 'Synchronize data from external source'), + ('export', 'Export data to external system'), + ] +``` + +For dynamic registration (e.g. when actions depend on runtime state), you can call `register_model_actions()` directly, typically in your plugin's `ready()` method: ```python # __init__.py @@ -29,7 +44,7 @@ class MyPluginConfig(PluginConfig): config = MyPluginConfig ``` -Once registered, these actions will appear grouped under your model's name when creating or editing an ObjectPermission that includes your model as an object type. +Once registered, these actions appear as checkboxes in a flat list when creating or editing an ObjectPermission. ::: utilities.permissions.ModelAction diff --git a/netbox/templates/users/panels/custom_actions.html b/netbox/templates/users/panels/actions.html similarity index 64% rename from netbox/templates/users/panels/custom_actions.html rename to netbox/templates/users/panels/actions.html index 3ce1fd5e2..4fa7c28b7 100644 --- a/netbox/templates/users/panels/custom_actions.html +++ b/netbox/templates/users/panels/actions.html @@ -3,12 +3,18 @@ {% block panel_content %}
| {{ label }} | +{% checkmark enabled %} | +
|---|---|
| {{ action }} |
- {% checkmark True %}
+ {% checkmark enabled %}
{% if models %}
{{ models }}
{% endif %}
diff --git a/netbox/users/ui/panels.py b/netbox/users/ui/panels.py
index 4e1106def..2b396db0b 100644
--- a/netbox/users/ui/panels.py
+++ b/netbox/users/ui/panels.py
@@ -47,54 +47,44 @@ class ObjectPermissionPanel(panels.ObjectAttributesPanel):
enabled = attrs.BooleanAttr('enabled')
-class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
+class ObjectPermissionActionsPanel(panels.ObjectPanel):
+ template_name = 'users/panels/actions.html'
title = _('Actions')
- can_view = attrs.BooleanAttr('can_view', label=_('View'))
- can_add = attrs.BooleanAttr('can_add', label=_('Add'))
- can_change = attrs.BooleanAttr('can_change', label=_('Change'))
- can_delete = attrs.BooleanAttr('can_delete', label=_('Delete'))
-
-
-class ObjectPermissionCustomActionsPanel(panels.ObjectPanel):
- """
- A panel which displays non-CRUD (custom) actions assigned to an ObjectPermission.
- """
- template_name = 'users/panels/custom_actions.html'
- title = _('Custom Actions')
-
def get_context(self, context):
obj = context['object']
- custom_actions = [a for a in obj.actions if a not in RESERVED_ACTIONS]
- # Build a list of (action_name, model_labels) tuples from the registry,
- # scoped to the object types assigned to this permission.
- assigned_types = {
- f'{ot.app_label}.{ot.model}' for ot in obj.object_types.all()
- }
+ crud_actions = [
+ (_('View'), 'view' in obj.actions),
+ (_('Add'), 'add' in obj.actions),
+ (_('Change'), 'change' in obj.actions),
+ (_('Delete'), 'delete' in obj.actions),
+ ]
+
+ enabled_actions = set(obj.actions) - set(RESERVED_ACTIONS)
+
+ # Collect all registered actions from the full registry, deduplicating by name.
+ seen = []
+ seen_set = set()
action_models = {}
for model_key, model_actions in registry['model_actions'].items():
- if model_key in assigned_types:
- for action in model_actions:
- if action.name in custom_actions:
- action_models.setdefault(action.name, []).append(model_key)
+ for action in model_actions:
+ if action.name not in seen_set:
+ seen.append(action.name)
+ seen_set.add(action.name)
+ action_models.setdefault(action.name, []).append(model_key)
- custom_actions_display = [
- (action, ', '.join(action_models.get(action, [])))
- for action in custom_actions
+ registered_display = [
+ (action, action in enabled_actions, ', '.join(sorted(action_models[action])))
+ for action in seen
]
return {
**super().get_context(context),
- 'custom_actions': custom_actions_display,
+ 'crud_actions': crud_actions,
+ 'registered_actions': registered_display,
}
- def render(self, context):
- ctx = self.get_context(context)
- if not ctx['custom_actions']:
- return ''
- return super().render(context)
-
class OwnerPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
diff --git a/netbox/users/views.py b/netbox/users/views.py
index 89e54ef82..59ebe518f 100644
--- a/netbox/users/views.py
+++ b/netbox/users/views.py
@@ -272,7 +272,6 @@ class ObjectPermissionView(generic.ObjectView):
left_panels=[
panels.ObjectPermissionPanel(),
panels.ObjectPermissionActionsPanel(),
- panels.ObjectPermissionCustomActionsPanel(),
JSONPanel('constraints', title=_('Constraints')),
],
right_panels=[
diff --git a/netbox/utilities/forms/widgets/select.py b/netbox/utilities/forms/widgets/select.py
index 4941fe7f8..70bba98ce 100644
--- a/netbox/utilities/forms/widgets/select.py
+++ b/netbox/utilities/forms/widgets/select.py
@@ -218,7 +218,7 @@ class ObjectTypeSelectedOptions(ObjectTypeSelectMultiple):
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
- context['widget']['attrs']['data-object-types-selected'] = True
+ context['widget']['attrs']['data-object-types-selected'] = 'true'
return context
|