diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 281a5787c..c02f71329 100644 --- a/netbox/dcim/forms.py +++ b/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 # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index f8d2dfc70..fbb678135 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -154,6 +154,7 @@ urlpatterns = [ url(r'^console-server-ports/(?P\d+)/disconnect/$', views.ConsoleServerPortDisconnectView.as_view(), name='consoleserverport_disconnect'), url(r'^console-server-ports/(?P\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), url(r'^console-server-ports/(?P\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\d+)/disconnect/$', views.PowerOutletDisconnectView.as_view(), name='poweroutlet_disconnect'), url(r'^power-outlets/(?P\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), url(r'^power-outlets/(?P\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\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'), url(r'^interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), url(r'^interfaces/(?P\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\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), url(r'^device-bays/(?P\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), url(r'^device-bays/(?P\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\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c11341ce9..ac2e1fc00 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/templates/dcim/bulk_rename.html b/netbox/templates/dcim/bulk_rename.html new file mode 100644 index 000000000..d1db6ca07 --- /dev/null +++ b/netbox/templates/dcim/bulk_rename.html @@ -0,0 +1,56 @@ +{% extends '_base.html' %} +{% load helpers %} +{% load form_helpers %} + +{% block content %} +

{% block title %}Renaming {{ selected_objects|length }} {{ obj_type_plural|bettertitle }}{% endblock %}

+
+
+ + + + + + + + + {% for obj in selected_objects %} + + + + + {% endfor %} + +
Current NameNew Name
{{ obj.name }}{{ obj.new_name }}
+
+
+
+ {% csrf_token %} + {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %} +
+
Rename
+
+ {% render_form form %} +
+
+
+
+ {% if '_preview' in request.POST and not form.errors %} + + {% else %} + + {% endif %} + Cancel +
+
+
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index cdc0519c2..6c596a236 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -398,23 +398,26 @@ {% endfor %} - {% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %} - - {% endif %} + {% if perms.dcim.delete_devicebay %} @@ -460,33 +463,34 @@ {% endfor %} - {% if perms.dcim.add_interface or perms.dcim.delete_interface %} - - {% endif %} + {% if perms.dcim.delete_interface %} @@ -522,28 +526,29 @@ {% endfor %} - {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %} - - {% endif %} + {% if perms.dcim.delete_consoleserverport %} @@ -579,28 +584,29 @@ {% endfor %} - {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %} - - {% endif %} + {% if perms.dcim.delete_poweroutlet %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 536452a69..944792705 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -265,6 +265,9 @@ {% if perms.dcim.add_interface or perms.dcim.delete_interface %}