mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-28 11:47:35 +02:00
Consolidate ObjectPermission detail view actions panel
Merge ObjectPermissionActionsPanel and ObjectPermissionCustomActionsPanel into a single Actions panel that shows CRUD booleans and all registered actions in one table, matching the form's consolidated layout. Also fix data-object-types-selected attribute value (True -> 'true') and update plugin docs to show Meta.permissions as the primary registration approach.
This commit is contained in:
@@ -6,7 +6,22 @@ For example, a plugin might define a "sync" action for a model that syncs data f
|
|||||||
|
|
||||||
## Registering Model Actions
|
## 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
|
```python
|
||||||
# __init__.py
|
# __init__.py
|
||||||
@@ -29,7 +44,7 @@ class MyPluginConfig(PluginConfig):
|
|||||||
config = MyPluginConfig
|
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
|
::: utilities.permissions.ModelAction
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
|
|
||||||
{% block panel_content %}
|
{% block panel_content %}
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for action, models in custom_actions %}
|
{% for label, enabled in crud_actions %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ label }}</th>
|
||||||
|
<td>{% checkmark enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for action, enabled, models in registered_actions %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ action }}</th>
|
<th scope="row">{{ action }}</th>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
{% checkmark True %}
|
{% checkmark enabled %}
|
||||||
{% if models %}
|
{% if models %}
|
||||||
<small class="text-muted">{{ models }}</small>
|
<small class="text-muted">{{ models }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -47,54 +47,44 @@ class ObjectPermissionPanel(panels.ObjectAttributesPanel):
|
|||||||
enabled = attrs.BooleanAttr('enabled')
|
enabled = attrs.BooleanAttr('enabled')
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
|
class ObjectPermissionActionsPanel(panels.ObjectPanel):
|
||||||
|
template_name = 'users/panels/actions.html'
|
||||||
title = _('Actions')
|
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):
|
def get_context(self, context):
|
||||||
obj = context['object']
|
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,
|
crud_actions = [
|
||||||
# scoped to the object types assigned to this permission.
|
(_('View'), 'view' in obj.actions),
|
||||||
assigned_types = {
|
(_('Add'), 'add' in obj.actions),
|
||||||
f'{ot.app_label}.{ot.model}' for ot in obj.object_types.all()
|
(_('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 = {}
|
action_models = {}
|
||||||
for model_key, model_actions in registry['model_actions'].items():
|
for model_key, model_actions in registry['model_actions'].items():
|
||||||
if model_key in assigned_types:
|
for action in model_actions:
|
||||||
for action in model_actions:
|
if action.name not in seen_set:
|
||||||
if action.name in custom_actions:
|
seen.append(action.name)
|
||||||
action_models.setdefault(action.name, []).append(model_key)
|
seen_set.add(action.name)
|
||||||
|
action_models.setdefault(action.name, []).append(model_key)
|
||||||
|
|
||||||
custom_actions_display = [
|
registered_display = [
|
||||||
(action, ', '.join(action_models.get(action, [])))
|
(action, action in enabled_actions, ', '.join(sorted(action_models[action])))
|
||||||
for action in custom_actions
|
for action in seen
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**super().get_context(context),
|
**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):
|
class OwnerPanel(panels.ObjectAttributesPanel):
|
||||||
name = attrs.TextAttr('name')
|
name = attrs.TextAttr('name')
|
||||||
|
|||||||
@@ -272,7 +272,6 @@ class ObjectPermissionView(generic.ObjectView):
|
|||||||
left_panels=[
|
left_panels=[
|
||||||
panels.ObjectPermissionPanel(),
|
panels.ObjectPermissionPanel(),
|
||||||
panels.ObjectPermissionActionsPanel(),
|
panels.ObjectPermissionActionsPanel(),
|
||||||
panels.ObjectPermissionCustomActionsPanel(),
|
|
||||||
JSONPanel('constraints', title=_('Constraints')),
|
JSONPanel('constraints', title=_('Constraints')),
|
||||||
],
|
],
|
||||||
right_panels=[
|
right_panels=[
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class ObjectTypeSelectedOptions(ObjectTypeSelectMultiple):
|
|||||||
|
|
||||||
def get_context(self, name, value, attrs):
|
def get_context(self, name, value, attrs):
|
||||||
context = super().get_context(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
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user