Browse Source

Closes #1781: Enable bulk renaming of device components

Jeremy Stretch 7 years ago
parent
commit
fc7a43f23e

+ 24 - 0
netbox/dcim/forms.py

@@ -50,6 +50,14 @@ def get_device_by_name_or_pk(name):
     return device
 
 
+class BulkRenameForm(forms.Form):
+    """
+    An extendable form to be used for renaming device components in bulk.
+    """
+    find = forms.CharField()
+    replace = forms.CharField()
+
+
 #
 # Regions
 #
@@ -1346,6 +1354,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
         }
 
 
+class ConsoleServerPortBulkRenameForm(BulkRenameForm):
+    pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
     pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
 
@@ -1607,6 +1619,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
         }
 
 
+class PowerOutletBulkRenameForm(BulkRenameForm):
+    pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 class PowerOutletBulkDisconnectForm(ConfirmationForm):
     pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
 
@@ -1936,6 +1952,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm, ChainedFieldsMixin):
         self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
 
 
+class InterfaceBulkRenameForm(BulkRenameForm):
+    pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 class InterfaceBulkDisconnectForm(ConfirmationForm):
     pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
 
@@ -2143,6 +2163,10 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
         ).exclude(pk=device_bay.device.pk)
 
 
+class DeviceBayBulkRenameForm(BulkRenameForm):
+    pk = forms.ModelMultipleChoiceField(queryset=DeviceBay.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 #
 # Connections
 #

+ 4 - 0
netbox/dcim/urls.py

@@ -154,6 +154,7 @@ urlpatterns = [
     url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.ConsoleServerPortDisconnectView.as_view(), name='consoleserverport_disconnect'),
     url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
     url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
+    url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
 
     # Power ports
     url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
@@ -173,6 +174,7 @@ urlpatterns = [
     url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.PowerOutletDisconnectView.as_view(), name='poweroutlet_disconnect'),
     url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
     url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
+    url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
 
     # Interfaces
     url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
@@ -184,6 +186,7 @@ urlpatterns = [
     url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
     url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
     url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
+    url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
 
     # Device bays
     url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
@@ -193,6 +196,7 @@ urlpatterns = [
     url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
     url(r'^device-bays/(?P<pk>\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
     url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
+    url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
 
     # Inventory items
     url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),

+ 69 - 1
netbox/dcim/views.py

@@ -12,7 +12,7 @@ from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.html import escape
-from django.utils.http import urlencode
+from django.utils.http import is_safe_url, urlencode
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 from natsort import natsorted
@@ -37,6 +37,50 @@ from .models import (
 )
 
 
+class BulkRenameView(View):
+    """
+    An extendable view for renaming device components in bulk.
+    """
+    model = None
+    form = None
+    template_name = 'dcim/bulk_rename.html'
+
+    def post(self, request):
+
+        return_url = request.GET.get('return_url')
+        if not return_url or not is_safe_url(url=return_url, host=request.get_host()):
+            return_url = 'home'
+
+        if '_preview' in request.POST or '_apply' in request.POST:
+            form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
+            selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
+
+            if form.is_valid():
+                for obj in selected_objects:
+                    obj.new_name = obj.name.replace(form.cleaned_data['find'], form.cleaned_data['replace'])
+
+                if '_apply' in request.POST:
+                    for obj in selected_objects:
+                        obj.name = obj.new_name
+                        obj.save()
+                    messages.success(request, "Renamed {} {}".format(
+                        len(selected_objects),
+                        self.model._meta.verbose_name_plural
+                    ))
+                    return redirect(return_url)
+
+        else:
+            form = self.form(initial={'pk': request.POST.getlist('pk')})
+            selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
+
+        return render(request, self.template_name, {
+            'form': form,
+            'obj_type_plural': self.model._meta.verbose_name_plural,
+            'selected_objects': selected_objects,
+            'return_url': return_url,
+        })
+
+
 class BulkDisconnectView(View):
     """
     An extendable view for disconnection console/power/interface components in bulk.
@@ -1270,6 +1314,12 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = ConsoleServerPort
 
 
+class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
+    permission_required = 'dcim.change_consoleserverport'
+    model = ConsoleServerPort
+    form = forms.ConsoleServerPortBulkRenameForm
+
+
 class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
     permission_required = 'dcim.change_consoleserverport'
     model = ConsoleServerPort
@@ -1548,6 +1598,12 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = PowerOutlet
 
 
+class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView):
+    permission_required = 'dcim.change_poweroutlet'
+    model = PowerOutlet
+    form = forms.PowerOutletBulkRenameForm
+
+
 class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
     permission_required = 'dcim.change_poweroutlet'
     model = PowerOutlet
@@ -1612,6 +1668,12 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
     form = forms.InterfaceBulkEditForm
 
 
+class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
+    permission_required = 'dcim.change_interface'
+    model = Interface
+    form = forms.InterfaceBulkRenameForm
+
+
 class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_interface'
     cls = Interface
@@ -1713,6 +1775,12 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
         })
 
 
+class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
+    permission_required = 'dcim.change_devicebay'
+    model = DeviceBay
+    form = forms.DeviceBayBulkRenameForm
+
+
 class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_devicebay'
     cls = DeviceBay

+ 56 - 0
netbox/templates/dcim/bulk_rename.html

@@ -0,0 +1,56 @@
+{% extends '_base.html' %}
+{% load helpers %}
+{% load form_helpers %}
+
+{% block content %}
+    <h1>{% block title %}Renaming {{ selected_objects|length }} {{ obj_type_plural|bettertitle }}{% endblock %}</h1>
+    <div class="row">
+        <div class="col-md-7">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th>Current Name</th>
+                        <th>New Name</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for obj in selected_objects %}
+                        <tr>
+                            <td>{{ obj.name }}</td>
+                            <td>{{ obj.new_name }}</td>
+                        </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+        <div class="col-md-5">
+            <form action="" method="post" class="form form-horizontal">
+                {% csrf_token %}
+                {% if form.non_field_errors %}
+                    <div class="panel panel-danger">
+                        <div class="panel-heading"><strong>Errors</strong></div>
+                        <div class="panel-body">
+                            {{ form.non_field_errors }}
+                        </div>
+                    </div>
+                {% endif %}
+                <div class="panel panel-default">
+                    <div class="panel-heading"><strong>Rename</strong></div>
+                    <div class="panel-body">
+                        {% render_form form %}
+                    </div>
+                </div>
+                <div class="form-group text-right">
+                    <div class="col-md-12">
+                        {% if '_preview' in request.POST and not form.errors %}
+                            <button type="submit" name="_apply" class="btn btn-primary">Apply</button>
+                        {% else %}
+                            <button type="submit" name="_preview" class="btn btn-primary">Preview</button>
+                        {% endif %}
+                        <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
+                    </div>
+                </div>
+            </form>
+        </div>
+    </div>
+{% endblock %}

+ 94 - 88
netbox/templates/dcim/device.html

@@ -398,23 +398,26 @@
                         {% endfor %}
                     </tbody>
                 </table>
-                {% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
-                    <div class="panel-footer">
-                        {% if device_bays and perms.dcim.delete_devicebay %}
-                            <button type="submit" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
-                            </button>
-                        {% endif %}
-                        {% if perms.dcim.add_devicebay %}
-                            <div class="pull-right">
-                                <a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
-                                </a>
-                            </div>
-                            <div class="clearfix"></div>
-                        {% endif %}
-                     </div>
-                {% endif %}
+                <div class="panel-footer">
+                    {% if device_bays and perms.dcim.change_devicebay %}
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                        </button>
+                    {% endif %}
+                    {% if device_bays and perms.dcim.delete_devicebay %}
+                        <button type="submit" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                        </button>
+                    {% endif %}
+                    {% if perms.dcim.add_devicebay %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
+                            </a>
+                        </div>
+                        <div class="clearfix"></div>
+                    {% endif %}
+                 </div>
             </div>
             {% if perms.dcim.delete_devicebay %}
                 </form>
@@ -460,33 +463,34 @@
                         {% endfor %}
                     </tbody>
                 </table>
-                {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
-                    <div class="panel-footer">
-                        {% if interfaces and perms.dcim.change_interface %}
-                            <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
-                                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                            </button>
-                        {% endif %}
-                        {% if interfaces and perms.dcim.delete_interfaceconnection %}
-                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                            </button>
-                        {% endif %}
-                        {% if interfaces and perms.dcim.delete_interface %}
-                            <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                            </button>
-                        {% endif %}
-                        {% if perms.dcim.add_interface %}
-                            <div class="pull-right">
-                                <a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
-                                </a>
-                            </div>
-                            <div class="clearfix"></div>
-                        {% endif %}
-                     </div>
-                {% endif %}
+                <div class="panel-footer">
+                    {% if interfaces and perms.dcim.change_interface %}
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                        </button>
+                        <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                        </button>
+                    {% endif %}
+                    {% if interfaces and perms.dcim.delete_interfaceconnection %}
+                        <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                        </button>
+                    {% endif %}
+                    {% if interfaces and perms.dcim.delete_interface %}
+                        <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                        </button>
+                    {% endif %}
+                    {% if perms.dcim.add_interface %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
+                            </a>
+                        </div>
+                        <div class="clearfix"></div>
+                    {% endif %}
+                 </div>
             </div>
             {% if perms.dcim.delete_interface %}
                 </form>
@@ -522,28 +526,29 @@
                         {% endfor %}
                     </tbody>
                 </table>
-                {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
-                    <div class="panel-footer">
-                        {% if cs_ports and perms.dcim.change_consoleport %}
-                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                            </button>
-                        {% endif %}
-                        {% if cs_ports and perms.dcim.delete_consoleserverport %}
-                            <button type="submit" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                            </button>
-                        {% endif %}
-                        {% if perms.dcim.add_consoleserverport %}
-                            <div class="pull-right">
-                                <a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
-                                </a>
-                            </div>
-                            <div class="clearfix"></div>
-                        {% endif %}
-                    </div>
-                {% endif %}
+                <div class="panel-footer">
+                    {% if cs_ports and perms.dcim.change_consoleport %}
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                        </button>
+                        <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                        </button>
+                    {% endif %}
+                    {% if cs_ports and perms.dcim.delete_consoleserverport %}
+                        <button type="submit" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                        </button>
+                    {% endif %}
+                    {% if perms.dcim.add_consoleserverport %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
+                            </a>
+                        </div>
+                        <div class="clearfix"></div>
+                    {% endif %}
+                </div>
             </div>
             {% if perms.dcim.delete_consoleserverport %}
                 </form>
@@ -579,28 +584,29 @@
                         {% endfor %}
                     </tbody>
                 </table>
-                {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
-                    <div class="panel-footer">
-                        {% if power_outlets and perms.dcim.change_powerport %}
-                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                            </button>
-                        {% endif %}
-                        {% if power_outlets and perms.dcim.delete_poweroutlet %}
-                            <button type="submit" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                            </button>
-                        {% endif %}
-                        {% if perms.dcim.add_poweroutlet %}
-                            <div class="pull-right">
-                                <a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
-                                </a>
-                            </div>
-                            <div class="clearfix"></div>
-                        {% endif %}
-                    </div>
-                {% endif %}
+                <div class="panel-footer">
+                    {% if power_outlets and perms.dcim.change_powerport %}
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                        </button>
+                        <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                        </button>
+                    {% endif %}
+                    {% if power_outlets and perms.dcim.delete_poweroutlet %}
+                        <button type="submit" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                        </button>
+                    {% endif %}
+                    {% if perms.dcim.add_poweroutlet %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
+                            </a>
+                        </div>
+                        <div class="clearfix"></div>
+                    {% endif %}
+                </div>
             </div>
             {% if perms.dcim.delete_poweroutlet %}
                 </form>

+ 3 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -265,6 +265,9 @@
             {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
                 <div class="panel-footer">
                     {% if interfaces and perms.dcim.change_interface %}
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ vm.get_absolute_url }}" class="btn btn-warning btn-xs">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                        </button>
                         <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=vm.pk %}" class="btn btn-warning btn-xs">
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
                         </button>