mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Initial work on #19735
This commit is contained in:
parent
71e6ea5785
commit
c438c13045
123
netbox/netbox/object_actions.py
Normal file
123
netbox/netbox/object_actions.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
|
from extras.models import ExportTemplate
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Add',
|
||||||
|
'BulkDelete',
|
||||||
|
'BulkEdit',
|
||||||
|
'BulkExport',
|
||||||
|
'BulkImport',
|
||||||
|
'Delete',
|
||||||
|
'Edit',
|
||||||
|
'ObjectAction',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectAction:
|
||||||
|
name = ''
|
||||||
|
label = None
|
||||||
|
bulk = False
|
||||||
|
permissions_required = set()
|
||||||
|
url_kwargs = []
|
||||||
|
|
||||||
|
def get_context(self, context, obj):
|
||||||
|
viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{self.name}'
|
||||||
|
url = reverse(viewname, kwargs={kwarg: getattr(obj, kwarg) for kwarg in self.url_kwargs})
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Add(ObjectAction):
|
||||||
|
"""
|
||||||
|
Create a new object.
|
||||||
|
"""
|
||||||
|
name = 'add'
|
||||||
|
label = _('Add')
|
||||||
|
permissions_required = {'add'}
|
||||||
|
template_name = 'buttons/add.html'
|
||||||
|
|
||||||
|
|
||||||
|
class Edit(ObjectAction):
|
||||||
|
"""
|
||||||
|
Edit a single object.
|
||||||
|
"""
|
||||||
|
name = 'edit'
|
||||||
|
label = _('Edit')
|
||||||
|
permissions_required = {'change'}
|
||||||
|
url_kwargs = ['pk']
|
||||||
|
template_name = 'buttons/edit.html'
|
||||||
|
|
||||||
|
|
||||||
|
class Delete(ObjectAction):
|
||||||
|
"""
|
||||||
|
Delete a single object.
|
||||||
|
"""
|
||||||
|
name = 'delete'
|
||||||
|
label = _('Delete')
|
||||||
|
permissions_required = {'delete'}
|
||||||
|
url_kwargs = ['pk']
|
||||||
|
template_name = 'buttons/delete.html'
|
||||||
|
|
||||||
|
|
||||||
|
class BulkImport(ObjectAction):
|
||||||
|
"""
|
||||||
|
Import multiple objects at once.
|
||||||
|
"""
|
||||||
|
name = 'bulk_import'
|
||||||
|
label = _('Import')
|
||||||
|
permissions_required = {'add'}
|
||||||
|
template_name = 'buttons/import.html'
|
||||||
|
|
||||||
|
|
||||||
|
class BulkExport(ObjectAction):
|
||||||
|
"""
|
||||||
|
Export multiple objects at once.
|
||||||
|
"""
|
||||||
|
name = 'export'
|
||||||
|
label = _('Export')
|
||||||
|
permissions_required = {'view'}
|
||||||
|
template_name = 'buttons/export.html'
|
||||||
|
|
||||||
|
def get_context(self, context, model):
|
||||||
|
object_type = ObjectType.objects.get_for_model(model)
|
||||||
|
user = context['request'].user
|
||||||
|
|
||||||
|
# Determine if the "all data" export returns CSV or YAML
|
||||||
|
data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV'
|
||||||
|
|
||||||
|
# Retrieve all export templates for this model
|
||||||
|
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'perms': context['perms'],
|
||||||
|
'object_type': object_type,
|
||||||
|
'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
|
||||||
|
'export_templates': export_templates,
|
||||||
|
'data_format': data_format,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BulkEdit(ObjectAction):
|
||||||
|
"""
|
||||||
|
Change the value of one or more fields on a set of objects.
|
||||||
|
"""
|
||||||
|
name = 'bulk_edit'
|
||||||
|
label = _('Edit')
|
||||||
|
bulk = True
|
||||||
|
permissions_required = {'change'}
|
||||||
|
template_name = 'buttons/bulk_edit.html'
|
||||||
|
|
||||||
|
|
||||||
|
class BulkDelete(ObjectAction):
|
||||||
|
"""
|
||||||
|
Delete each of a set of objects.
|
||||||
|
"""
|
||||||
|
name = 'bulk_delete'
|
||||||
|
label = _('Delete')
|
||||||
|
bulk = True
|
||||||
|
permissions_required = {'delete'}
|
||||||
|
template_name = 'buttons/bulk_delete.html'
|
@ -22,6 +22,7 @@ from core.models import ObjectType
|
|||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
from extras.choices import CustomFieldUIEditableChoices
|
from extras.choices import CustomFieldUIEditableChoices
|
||||||
from extras.models import CustomField, ExportTemplate
|
from extras.models import CustomField, ExportTemplate
|
||||||
|
from netbox.object_actions import Add, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||||
@ -60,6 +61,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
template_name = 'generic/object_list.html'
|
template_name = 'generic/object_list.html'
|
||||||
filterset = None
|
filterset = None
|
||||||
filterset_form = None
|
filterset_form = None
|
||||||
|
actions = (Add, BulkImport, BulkEdit, BulkExport, BulkDelete)
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'view')
|
return get_permission_for_model(self.queryset.model, 'view')
|
||||||
@ -150,7 +152,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Determine the available actions
|
# Determine the available actions
|
||||||
actions = self.get_permitted_actions(request.user)
|
actions = self.get_permitted_actions(request.user)
|
||||||
has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
# has_bulk_actions = any([a.startswith('bulk_') for a in actions])
|
||||||
|
has_bulk_actions = True
|
||||||
|
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from extras.models import TableConfig
|
from extras.models import TableConfig
|
||||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -19,7 +18,7 @@ class ActionsMixin:
|
|||||||
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
|
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
|
||||||
with custom actions, such as bulk_sync.
|
with custom actions, such as bulk_sync.
|
||||||
"""
|
"""
|
||||||
actions = DEFAULT_ACTION_PERMISSIONS
|
# actions = DEFAULT_ACTION_PERMISSIONS
|
||||||
|
|
||||||
def get_permitted_actions(self, user, model=None):
|
def get_permitted_actions(self, user, model=None):
|
||||||
"""
|
"""
|
||||||
@ -30,13 +29,16 @@ class ActionsMixin:
|
|||||||
# Resolve required permissions for each action
|
# Resolve required permissions for each action
|
||||||
permitted_actions = []
|
permitted_actions = []
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
|
perms = action if type(action) is str else action.permissions_required # Backward compatibility
|
||||||
required_permissions = [
|
required_permissions = [
|
||||||
get_permission_for_model(model, name) for name in self.actions.get(action, set())
|
get_permission_for_model(model, perm) for perm in perms
|
||||||
]
|
]
|
||||||
if not required_permissions or user.has_perms(required_permissions):
|
if not required_permissions or user.has_perms(required_permissions):
|
||||||
permitted_actions.append(action)
|
permitted_actions.append(action)
|
||||||
|
|
||||||
return permitted_actions
|
return {
|
||||||
|
action.name: action for action in permitted_actions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TableMixin:
|
class TableMixin:
|
||||||
|
@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
|
from netbox.object_actions import Add, BulkDelete, BulkEdit, BulkExport, BulkImport, Delete, Edit
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||||
@ -36,7 +37,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectView(BaseObjectView):
|
class ObjectView(ActionsMixin, BaseObjectView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single object for display.
|
Retrieve a single object for display.
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ class ObjectView(BaseObjectView):
|
|||||||
tab: A ViewTab instance for the view
|
tab: A ViewTab instance for the view
|
||||||
"""
|
"""
|
||||||
tab = None
|
tab = None
|
||||||
|
actions = (Edit, Delete)
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return get_permission_for_model(self.queryset.model, 'view')
|
return get_permission_for_model(self.queryset.model, 'view')
|
||||||
@ -72,9 +74,11 @@ class ObjectView(BaseObjectView):
|
|||||||
request: The current request
|
request: The current request
|
||||||
"""
|
"""
|
||||||
instance = self.get_object(**kwargs)
|
instance = self.get_object(**kwargs)
|
||||||
|
actions = self.get_permitted_actions(request.user, model=instance)
|
||||||
|
|
||||||
return render(request, self.get_template_name(), {
|
return render(request, self.get_template_name(), {
|
||||||
'object': instance,
|
'object': instance,
|
||||||
|
'actions': actions,
|
||||||
'tab': self.tab,
|
'tab': self.tab,
|
||||||
**self.get_extra_context(request, instance),
|
**self.get_extra_context(request, instance),
|
||||||
})
|
})
|
||||||
@ -97,6 +101,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
table = None
|
table = None
|
||||||
filterset = None
|
filterset = None
|
||||||
filterset_form = None
|
filterset_form = None
|
||||||
|
actions = (Add, BulkImport, BulkEdit, BulkExport, BulkDelete)
|
||||||
template_name = 'generic/object_children.html'
|
template_name = 'generic/object_children.html'
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
|
@ -80,15 +80,9 @@ Context:
|
|||||||
{% if perms.extras.add_subscription and object.subscriptions %}
|
{% if perms.extras.add_subscription and object.subscriptions %}
|
||||||
{% subscribe_button object %}
|
{% subscribe_button object %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user|can_add:object %}
|
{% for name, action in actions.items %}
|
||||||
{% clone_button object %}
|
{% action_button action object %}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% if request.user|can_change:object %}
|
|
||||||
{% edit_button object %}
|
|
||||||
{% endif %}
|
|
||||||
{% if request.user|can_delete:object %}
|
|
||||||
{% delete_button object %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock control-buttons %}
|
{% endblock control-buttons %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -31,15 +31,11 @@ Context:
|
|||||||
<div class="btn-list">
|
<div class="btn-list">
|
||||||
{% plugin_list_buttons model %}
|
{% plugin_list_buttons model %}
|
||||||
{% block extra_controls %}{% endblock %}
|
{% block extra_controls %}{% endblock %}
|
||||||
{% if 'add' in actions %}
|
{% for name, action in actions.items %}
|
||||||
{% add_button model %}
|
{% if not action.bulk %}
|
||||||
{% endif %}
|
{% action_button action model %}
|
||||||
{% if 'bulk_import' in actions %}
|
{% endif %}
|
||||||
{% import_button model %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
{% if 'export' in actions %}
|
|
||||||
{% export_button model %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock controls %}
|
{% endblock controls %}
|
||||||
|
|
||||||
@ -91,12 +87,11 @@ Context:
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="bulk-action-buttons">
|
<div class="bulk-action-buttons">
|
||||||
{% if 'bulk_edit' in actions %}
|
{% for name, action in actions.items %}
|
||||||
{% bulk_edit_button model query_params=request.GET %}
|
{% if action.bulk %}
|
||||||
{% endif %}
|
{% bulk_action_button action model %}
|
||||||
{% if 'bulk_delete' in actions %}
|
{% endif %}
|
||||||
{% bulk_delete_button model query_params=request.GET %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -124,12 +119,11 @@ Context:
|
|||||||
<div class="btn-list d-print-none">
|
<div class="btn-list d-print-none">
|
||||||
{% block bulk_buttons %}
|
{% block bulk_buttons %}
|
||||||
<div class="bulk-action-buttons">
|
<div class="bulk-action-buttons">
|
||||||
{% if 'bulk_edit' in actions %}
|
{% for name, action in actions.items %}
|
||||||
{% bulk_edit_button model query_params=request.GET %}
|
{% if action.bulk %}
|
||||||
{% endif %}
|
{% bulk_action_button action model %}
|
||||||
{% if 'bulk_delete' in actions %}
|
{% endif %}
|
||||||
{% bulk_delete_button model query_params=request.GET %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.template import loader
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
@ -9,8 +10,10 @@ from utilities.querydict import prepare_cloned_fields
|
|||||||
from utilities.views import get_viewname
|
from utilities.views import get_viewname
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'action_button',
|
||||||
'add_button',
|
'add_button',
|
||||||
'bookmark_button',
|
'bookmark_button',
|
||||||
|
'bulk_action_button',
|
||||||
'bulk_delete_button',
|
'bulk_delete_button',
|
||||||
'bulk_edit_button',
|
'bulk_edit_button',
|
||||||
'clone_button',
|
'clone_button',
|
||||||
@ -217,3 +220,13 @@ def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
|
|||||||
'htmx_navigation': context.get('htmx_navigation'),
|
'htmx_navigation': context.get('htmx_navigation'),
|
||||||
'url': url,
|
'url': url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def action_button(context, action, obj):
|
||||||
|
return loader.render_to_string(action.template_name, action.get_context(context, obj))
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def bulk_action_button(context, action, model):
|
||||||
|
return loader.render_to_string(action.template_name, action.get_context(context, model))
|
||||||
|
Loading…
Reference in New Issue
Block a user