Repository: netbox-community/netbox
Pull request: PR for #8723
<!--
    Thank you for your interest in contributing to NetBox! Please note
    that our contribution policy requires that a feature request or bug
    report be opened for approval prior to filing a pull request. This
    helps avoid wasting time and effort on something that we might not
    be able to accept.

    Please indicate the relevant feature request or bug report below.
    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
    FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
-->
### Fixes: #8723 
<!--
    Please include a summary of the proposed changes below.
-->

WIP of implementing bulk rename for devices. Devices without a name need special handling implemented still.


Problem statement:
Bulk-rename for devices
### NetBox version

v3.1.6

### Feature type

New functionality

### Proposed functionality

Currently there is no bulk-rename functionality for devices. There is only a bulk-rename for interfaces. I would like to request to get the same bulk-rename functionality with (regex-)search/replace for devices.

### Use case

We are currently facing a renumbering of the rooms and buildings of our organisation to a more unique scheme. As our devices names in netbox contain some location info it would be a big help to rename the devices with the bulk-rename functionality. 

### Database changes

_No response_

### External dependencies

_No response_

Hints from issue discussion:
I‘m missing the bulk-rename functionality for devices as well. Example: When we need to change naming of patch panels.
This feature would be super useful for us as well. Being able to update the names for multiple devices when they "move" using a search/replace feature (e.g. updating the hostname/location prefix) would helpful. 
This probably applies to roughly any object with a `name` field; not just devices. (As noted, it exists already for device components.)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
@jeremystretch  I'll take a crack at this issue.
@cpund - All yours.
Got the initial work for renaming devices done I believe. Just trying to understand how to properly craft the return_url. I could just generically return the user to /dcim/devices/, but if they originally arrived at rename via a filtered /dcim/devices/ page from a site or rack, or whichever, it would just dump them to the general view rather than that original filtered view.
@cpund feel free to submit a PR with what you have now, and I'm happy to suggest a patch for the return URL (assuming there _is_ a feasible approach).
@jeremystretch Yeah, I'm going to spend around an hour or so possibly more just reading up further on how template rendering, particularly using variables in template rendering, works with Django, to see if I can achieve this. But as you mention, there may not a feasible approach - I just don't simply know enough about Django to figure out what variables are available for use, or if I could possibly inject them to be used.
Figured out how to return. Realized I could just use the request.GET.urlencode to bring that in. However, decided to see what would happen if I tried to rename a device without a name - spoiler: it doesn't like that. So now I'm trying to decide how best to factor in devices without a name, or maybe I just exclude them from being shown in the BulkRenameView.

Commit message: draft for error handling on device with no name

Patch to review:
diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py
index c5cd0fa65f2..f00bd73e110 100644
--- a/netbox/dcim/urls.py
+++ b/netbox/dcim/urls.py
@@ -248,6 +248,7 @@
     path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
     path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
     path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
+    path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'),
     path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
     path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
     path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 35a1056b211..28325bcfc9a 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1786,6 +1786,12 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
     table = tables.DeviceTable
 
 
+class DeviceBulkRenameView(generic.BulkRenameView):
+    queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
+    filterset = filtersets.DeviceFilterSet
+    table = tables.DeviceTable
+
+
 #
 # Devices
 #
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index 96efc0de744..f17bc179d72 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -676,6 +676,9 @@ def post(self, request):
         else:
             form = self.form(initial={'pk': request.POST.getlist('pk')})
             selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
+            for object in selected_objects:
+                # Do something to raise error message to user
+                pass
 
         return render(request, self.template_name, {
             'form': form,
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html
index 60efc842e26..a576a1f9e3f 100644
--- a/netbox/templates/dcim/device_list.html
+++ b/netbox/templates/dcim/device_list.html
@@ -2,6 +2,9 @@
 
 {% block bulk_buttons %}
   {% if perms.dcim.change_device %}
+    <button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-sm">
+      <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+    </button>
     <div class="dropdown">
       <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components


Code review comments:
diff_hunk: @@ -1786,6 +1786,12 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
     table = tables.DeviceTable
 
 
+class DeviceBulkRenameView(generic.BulkRenameView):
+    queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
path: netbox/dcim/views.py
text: @user1:
We can omit `prefetch_related()` here since we're only dealing with the object names.

```suggestion
    queryset = Device.objects.all()
```

@author:
Done.

diff_hunk: @@ -676,6 +676,9 @@ def post(self, request):
         else:
             form = self.form(initial={'pk': request.POST.getlist('pk')})
             selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
+            for object in selected_objects:
+                # Do something to raise error message to user
+                pass
path: netbox/netbox/views/generic/bulk_views.py
text: @user1:
The view should already handle the necessary validation. Did you run into something that isn't covered?

@author:
I believe I was just trying to raise an error when selecting a device without a name. Going over things, I definitely prefer your previous suggestion of `obj.name or ''`. Removing these lines.