Initial work on #19735

This commit is contained in:
Jeremy Stretch 2025-06-20 17:19:36 -04:00
parent 71e6ea5785
commit c438c13045
7 changed files with 170 additions and 36 deletions

View 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'

View File

@ -22,6 +22,7 @@ from core.models import ObjectType
from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices
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.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@ -60,6 +61,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
template_name = 'generic/object_list.html'
filterset = None
filterset_form = None
actions = (Add, BulkImport, BulkEdit, BulkExport, BulkDelete)
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
@ -150,7 +152,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Determine the available actions
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:

View File

@ -1,7 +1,6 @@
from django.shortcuts import get_object_or_404
from extras.models import TableConfig
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from utilities.permissions import get_permission_for_model
__all__ = (
@ -19,7 +18,7 @@ class ActionsMixin:
Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
with custom actions, such as bulk_sync.
"""
actions = DEFAULT_ACTION_PERMISSIONS
# actions = DEFAULT_ACTION_PERMISSIONS
def get_permitted_actions(self, user, model=None):
"""
@ -30,13 +29,16 @@ class ActionsMixin:
# Resolve required permissions for each action
permitted_actions = []
for action in self.actions:
perms = action if type(action) is str else action.permissions_required # Backward compatibility
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):
permitted_actions.append(action)
return permitted_actions
return {
action.name: action for action in permitted_actions
}
class TableMixin:

View File

@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
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.exceptions import AbortRequest, PermissionsViolation
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.
@ -46,6 +47,7 @@ class ObjectView(BaseObjectView):
tab: A ViewTab instance for the view
"""
tab = None
actions = (Edit, Delete)
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
@ -72,9 +74,11 @@ class ObjectView(BaseObjectView):
request: The current request
"""
instance = self.get_object(**kwargs)
actions = self.get_permitted_actions(request.user, model=instance)
return render(request, self.get_template_name(), {
'object': instance,
'actions': actions,
'tab': self.tab,
**self.get_extra_context(request, instance),
})
@ -97,6 +101,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
table = None
filterset = None
filterset_form = None
actions = (Add, BulkImport, BulkEdit, BulkExport, BulkDelete)
template_name = 'generic/object_children.html'
def get_children(self, request, parent):

View File

@ -80,15 +80,9 @@ Context:
{% if perms.extras.add_subscription and object.subscriptions %}
{% subscribe_button object %}
{% endif %}
{% if request.user|can_add:object %}
{% clone_button object %}
{% endif %}
{% if request.user|can_change:object %}
{% edit_button object %}
{% endif %}
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
{% for name, action in actions.items %}
{% action_button action object %}
{% endfor %}
{% endblock control-buttons %}
</div>

View File

@ -31,15 +31,11 @@ Context:
<div class="btn-list">
{% plugin_list_buttons model %}
{% block extra_controls %}{% endblock %}
{% if 'add' in actions %}
{% add_button model %}
{% endif %}
{% if 'bulk_import' in actions %}
{% import_button model %}
{% endif %}
{% if 'export' in actions %}
{% export_button model %}
{% endif %}
{% for name, action in actions.items %}
{% if not action.bulk %}
{% action_button action model %}
{% endif %}
{% endfor %}
</div>
{% endblock controls %}
@ -91,12 +87,11 @@ Context:
</label>
</div>
<div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
{% for name, action in actions.items %}
{% if action.bulk %}
{% bulk_action_button action model %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
@ -124,12 +119,11 @@ Context:
<div class="btn-list d-print-none">
{% block bulk_buttons %}
<div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
{% for name, action in actions.items %}
{% if action.bulk %}
{% bulk_action_button action model %}
{% endif %}
{% endfor %}
</div>
{% endblock %}
</div>

View File

@ -1,5 +1,6 @@
from django import template
from django.contrib.contenttypes.models import ContentType
from django.template import loader
from django.urls import NoReverseMatch, reverse
from core.models import ObjectType
@ -9,8 +10,10 @@ from utilities.querydict import prepare_cloned_fields
from utilities.views import get_viewname
__all__ = (
'action_button',
'add_button',
'bookmark_button',
'bulk_action_button',
'bulk_delete_button',
'bulk_edit_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'),
'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))