diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 43497b85c..7144a6d73 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -251,6 +251,10 @@ class ConsolePortTemplateForm(forms.ModelForm, BootstrapMixin): fields = ['name_pattern'] +class ConsolePortTemplateBulkDeleteForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=ConsolePortTemplate.objects.all(), widget=forms.MultipleHiddenInput) + + class ConsoleServerPortTemplateForm(forms.ModelForm, BootstrapMixin): name_pattern = ExpandableNameField(label='Name') @@ -259,6 +263,10 @@ class ConsoleServerPortTemplateForm(forms.ModelForm, BootstrapMixin): fields = ['name_pattern'] +class ConsoleServerPortTemplateBulkDeleteForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPortTemplate.objects.all(), widget=forms.MultipleHiddenInput) + + class PowerPortTemplateForm(forms.ModelForm, BootstrapMixin): name_pattern = ExpandableNameField(label='Name') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 52bfcdfdb..dd0cf0504 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -50,31 +50,29 @@ urlpatterns = [ url(r'^device-types/(?P\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), - # Component templates - url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), - name='devicetype_add_consoleport'), - url(r'^device-types/(?P\d+)/console-ports/delete/$', views.component_template_delete, - {'model': ConsolePortTemplate}, name='devicetype_delete_consoleport'), - url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), - name='devicetype_add_consoleserverport'), - url(r'^device-types/(?P\d+)/console-server-ports/delete/$', views.component_template_delete, - {'model': ConsoleServerPortTemplate}, name='devicetype_delete_consoleserverport'), - url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), - name='devicetype_add_powerport'), - url(r'^device-types/(?P\d+)/power-ports/delete/$', views.component_template_delete, - {'model': PowerPortTemplate}, name='devicetype_delete_powerport'), - url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), - name='devicetype_add_poweroutlet'), - url(r'^device-types/(?P\d+)/power-outlets/delete/$', views.component_template_delete, - {'model': PowerOutletTemplate}, name='devicetype_delete_poweroutlet'), - url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), - name='devicetype_add_interface'), - url(r'^device-types/(?P\d+)/interfaces/delete/$', views.component_template_delete, - {'model': InterfaceTemplate}, name='devicetype_delete_interface'), - url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), - name='devicetype_add_devicebay'), - url(r'^device-types/(?P\d+)/device-bays/delete/$', views.component_template_delete, - {'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'), + # Console port templates + url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'), + url(r'^device-types/(?P\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), + + # Console server port templates + url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'), + url(r'^device-types/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), + + # Power port templates + url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'), + url(r'^device-types/(?P\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), + + # Power outlet templates + url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'), + url(r'^device-types/(?P\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), + + # Interface templates + url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'), + url(r'^device-types/(?P\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), + + # Device bay templates + url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'), + url(r'^device-types/(?P\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), # Device roles url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 390479895..73d21c293 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -7,8 +7,7 @@ from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse -from django.db.models import Count, ProtectedError, Sum -from django.forms import ModelMultipleChoiceField, MultipleHiddenInput +from django.db.models import Count, Sum from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.utils.http import urlencode @@ -17,7 +16,6 @@ from django.views.generic import View from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit from extras.models import TopologyMap -from utilities.error_handlers import handle_protectederror from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, @@ -396,68 +394,65 @@ class ConsolePortTemplateAddView(ComponentTemplateCreateView): form = forms.ConsolePortTemplateForm +class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_consoleporttemplate' + cls = ConsolePortTemplate + parent_cls = DeviceType + + class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView): model = ConsoleServerPortTemplate form = forms.ConsoleServerPortTemplateForm +class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_consoleserverporttemplate' + cls = ConsoleServerPortTemplate + parent_cls = DeviceType + + class PowerPortTemplateAddView(ComponentTemplateCreateView): model = PowerPortTemplate form = forms.PowerPortTemplateForm +class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_powerporttemplate' + cls = PowerPortTemplate + parent_cls = DeviceType + + class PowerOutletTemplateAddView(ComponentTemplateCreateView): model = PowerOutletTemplate form = forms.PowerOutletTemplateForm +class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_poweroutlettemplate' + cls = PowerOutletTemplate + parent_cls = DeviceType + + class InterfaceTemplateAddView(ComponentTemplateCreateView): model = InterfaceTemplate form = forms.InterfaceTemplateForm +class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_interfacetemplate' + cls = InterfaceTemplate + parent_cls = DeviceType + + class DeviceBayTemplateAddView(ComponentTemplateCreateView): model = DeviceBayTemplate form = forms.DeviceBayTemplateForm -def component_template_delete(request, pk, model): - - devicetype = get_object_or_404(DeviceType, pk=pk) - - class ComponentTemplateBulkDeleteForm(ConfirmationForm): - pk = ModelMultipleChoiceField(queryset=model.objects.all(), widget=MultipleHiddenInput) - - if '_confirm' in request.POST: - form = ComponentTemplateBulkDeleteForm(request.POST) - if form.is_valid(): - - # Delete component templates - objects_to_delete = model.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']]) - try: - deleted_count = objects_to_delete.count() - objects_to_delete.delete() - except ProtectedError, e: - handle_protectederror(list(objects_to_delete), request, e) - return redirect('dcim:devicetype', {'pk': devicetype.pk}) - - messages.success(request, "Deleted {} {}".format(deleted_count, model._meta.verbose_name_plural)) - return redirect('dcim:devicetype', pk=devicetype.pk) - - else: - form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')}) - - selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk')) - if not selected_objects: - messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural)) - return redirect('dcim:devicetype', pk=devicetype.pk) - - return render(request, 'dcim/component_template_delete.html', { - 'devicetype': devicetype, - 'form': form, - 'selected_objects': selected_objects, - 'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}), - }) +class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_devicebaytemplate' + cls = DeviceBayTemplate + parent_cls = DeviceType # diff --git a/netbox/templates/utilities/confirm_bulk_delete.html b/netbox/templates/utilities/confirm_bulk_delete.html index 49fb975a3..97b9cd277 100644 --- a/netbox/templates/utilities/confirm_bulk_delete.html +++ b/netbox/templates/utilities/confirm_bulk_delete.html @@ -5,11 +5,15 @@ {% block message %}

- Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}? + Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from {{ parent_obj }}{% endif %}?

    {% for obj in selected_objects %} -
  • {{ obj }}
  • + {% if obj.get_absolute_url %} +
  • {{ obj }}
  • + {% else %} +
  • {{ obj }}
  • + {% endif %} {% endfor %}
{% endblock %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 29f2de7cf..1ea23dfd2 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -3,9 +3,11 @@ from django_tables2 import RequestConfig from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from django.db import transaction, IntegrityError from django.db.models import ProtectedError +from django.forms import ModelMultipleChoiceField, MultipleHiddenInput from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.template import TemplateSyntaxError @@ -309,6 +311,7 @@ class BulkEditView(View): class BulkDeleteView(View): cls = None + parent_cls = None form = None template_name = 'utilities/confirm_bulk_delete.html' default_redirect_url = None @@ -317,24 +320,35 @@ class BulkDeleteView(View): def dispatch(self, *args, **kwargs): return super(BulkDeleteView, self).dispatch(*args, **kwargs) - def get(self, request, *args, **kwargs): - return redirect(self.default_redirect_url) - def post(self, request, *args, **kwargs): + # Attempt to derive parent object if a parent class has been given + if self.parent_cls: + parent_obj = get_object_or_404(self.parent_cls, **kwargs) + else: + parent_obj = None + + # Determine URL to redirect users upon deletion of objects posted_redirect_url = request.POST.get('redirect_url') if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()): redirect_url = posted_redirect_url - else: + elif parent_obj: + redirect_url = parent_obj.get_absolute_url() + elif self.default_redirect_url: redirect_url = reverse(self.default_redirect_url) + else: + raise ImproperlyConfigured('No redirect URL has been provided.') + # Are we deleting *all* objects in the queryset or just a selected subset? if request.POST.get('_all'): pk_list = [x for x in request.POST.get('pk_all').split(',') if x] else: pk_list = request.POST.getlist('pk') + form_cls = self.get_form() + if '_confirm' in request.POST: - form = self.form(request.POST) + form = form_cls(request.POST) if form.is_valid(): # Delete objects @@ -351,7 +365,7 @@ class BulkDeleteView(View): return redirect(redirect_url) else: - form = self.form(initial={'pk': pk_list}) + form = form_cls(initial={'pk': pk_list}) selected_objects = self.cls.objects.filter(pk__in=pk_list) if not selected_objects: @@ -360,7 +374,18 @@ class BulkDeleteView(View): return render(request, self.template_name, { 'form': form, + 'parent_obj': parent_obj, 'obj_type_plural': self.cls._meta.verbose_name_plural, 'selected_objects': selected_objects, 'cancel_url': redirect_url, }) + + def get_form(self): + """Provide a standard bulk delete form if none has been specified for the view""" + + class BulkDeleteForm(ConfirmationForm): + pk = ModelMultipleChoiceField(queryset=self.cls.objects.all(), widget=MultipleHiddenInput) + + if self.form: + return self.form + return BulkDeleteForm