From 2b68841f3134abbe5d8404ca1649c059f9d2bf3b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Jun 2025 10:37:25 -0400 Subject: [PATCH] Replace clone_button with an ObjectAction --- netbox/netbox/object_actions.py | 23 ++++++++++++- netbox/netbox/views/generic/object_views.py | 6 ++-- netbox/templates/generic/object.html | 3 -- netbox/utilities/templatetags/buttons.py | 37 +++++++++++---------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/netbox/netbox/object_actions.py b/netbox/netbox/object_actions.py index 507a491ec..b427d58cd 100644 --- a/netbox/netbox/object_actions.py +++ b/netbox/netbox/object_actions.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext as _ from core.models import ObjectType from extras.models import ExportTemplate +from utilities.querydict import prepare_cloned_fields __all__ = ( 'AddObject', @@ -11,6 +12,7 @@ __all__ = ( 'BulkExport', 'BulkImport', 'BulkRename', + 'CloneObject', 'DeleteObject', 'EditObject', 'ObjectAction', @@ -22,7 +24,7 @@ class ObjectAction: Base class for single- and multi-object operations. Params: - name: The action name + name: The action name appended to the module for view resolution label: Human-friendly label for the rendered button multi: Set to True if this action is performed by selecting multiple objects (i.e. using a table) permissions_required: The set of permissions a user must have to perform the action @@ -60,6 +62,25 @@ class AddObject(ObjectAction): template_name = 'buttons/add.html' +class CloneObject(ObjectAction): + """ + Populate the new object form with select details from an existing object. + """ + name = 'add' + label = _('Clone') + permissions_required = {'add'} + template_name = 'buttons/clone.html' + + @classmethod + def get_context(cls, context, obj): + param_string = prepare_cloned_fields(obj).urlencode() + url = f'{cls.get_url(obj)}?{param_string}' if param_string else None + return { + 'url': url, + 'label': cls.label, + } + + class EditObject(ObjectAction): """ Edit a single object. diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 67517b38b..2e9b73d20 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -14,7 +14,9 @@ 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 AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, DeleteObject, EditObject +from netbox.object_actions import ( + AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, CloneObject, DeleteObject, EditObject, +) from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.forms import ConfirmationForm, restrict_form_fields @@ -47,7 +49,7 @@ class ObjectView(ActionsMixin, BaseObjectView): tab: A ViewTab instance for the view """ tab = None - actions = (EditObject, DeleteObject) + actions = (CloneObject, EditObject, DeleteObject) def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'view') diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 64086855e..4bccf108c 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -80,9 +80,6 @@ Context: {% if perms.extras.add_subscription and object.subscriptions %} {% subscribe_button object %} {% endif %} - {% if request.user|can_add:object %} - {% clone_button object %} - {% endif %} {% action_buttons actions object %} {% endblock control-buttons %} diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index 77163f515..404386910 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -69,24 +69,6 @@ def bookmark_button(context, instance): } -@register.inclusion_tag('buttons/clone.html') -def clone_button(instance): - # Resolve URL path - viewname = get_viewname(instance, 'add') - try: - url = reverse(viewname) - except NoReverseMatch: - return { - 'url': None, - } - - # Populate cloned field values and return full URL - param_string = prepare_cloned_fields(instance).urlencode() - return { - 'url': f'{url}?{param_string}' if param_string else None, - } - - @register.inclusion_tag('buttons/subscribe.html', takes_context=True) def subscribe_button(context, instance): # Skip for objects which don't support notifications @@ -126,6 +108,25 @@ def subscribe_button(context, instance): # Legacy object buttons # +# TODO: Remove in NetBox v4.6 +@register.inclusion_tag('buttons/clone.html') +def clone_button(instance): + # Resolve URL path + viewname = get_viewname(instance, 'add') + try: + url = reverse(viewname) + except NoReverseMatch: + return { + 'url': None, + } + + # Populate cloned field values and return full URL + param_string = prepare_cloned_fields(instance).urlencode() + return { + 'url': f'{url}?{param_string}' if param_string else None, + } + + # TODO: Remove in NetBox v4.6 @register.inclusion_tag('buttons/edit.html') def edit_button(instance):