diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 7207e7648..aae8bb5f6 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -5,7 +5,7 @@ from django.db.models import Count from taggit.forms import TagField from dcim.models import Site, Device, Interface, Rack -from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -55,7 +55,7 @@ class ProviderCSVForm(forms.ModelForm): } -class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput) asn = forms.IntegerField(required=False, label='ASN') account = forms.CharField(max_length=30, required=False, label='Account number') @@ -158,7 +158,7 @@ class CircuitCSVForm(forms.ModelForm): ] -class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput) type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 057fc04c0..d617c6c04 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -10,7 +10,7 @@ from mptt.forms import TreeNodeChoiceField from taggit.forms import TagField from timezone_field import TimeZoneFormField -from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm from tenancy.models import Tenant @@ -170,7 +170,7 @@ class SiteCSVForm(forms.ModelForm): } -class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Site.objects.all(), widget=forms.MultipleHiddenInput @@ -403,7 +403,7 @@ class RackCSVForm(forms.ModelForm): ) -class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site') group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group') @@ -572,7 +572,7 @@ class DeviceTypeCSVForm(forms.ModelForm): } -class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) u_height = forms.IntegerField(min_value=1, required=False) @@ -1090,7 +1090,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name)) -class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput) device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type') device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role') diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index d50be029a..6e8e90512 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from mptt.forms import TreeNodeMultipleChoiceField +from taggit.forms import TagField from taggit.models import Tag from dcim.models import Region @@ -193,6 +194,16 @@ class TagForm(BootstrapMixin, forms.ModelForm): fields = ['name', 'slug'] +class AddRemoveTagsForm(forms.Form): + + def __init__(self, *args, **kwargs): + super(AddRemoveTagsForm, self).__init__(*args, **kwargs) + + # Add add/remove tags fields + self.fields['add_tags'] = TagField(required=False) + self.fields['remove_tags'] = TagField(required=False) + + # # Config contexts # diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2b5f8ed7a..8a2f815ad 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -7,7 +7,7 @@ from django.db.models import Count from taggit.forms import TagField from dcim.models import Site, Rack, Device, Interface -from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -68,7 +68,7 @@ class VRFCSVForm(forms.ModelForm): } -class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) enforce_unique = forms.NullBooleanField( @@ -153,7 +153,7 @@ class AggregateCSVForm(forms.ModelForm): fields = Aggregate.csv_headers -class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput) rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR') date_added = forms.DateField(required=False) @@ -346,7 +346,7 @@ class PrefixCSVForm(forms.ModelForm): raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid)) -class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') @@ -678,7 +678,7 @@ class IPAddressCSVForm(forms.ModelForm): return ipaddress -class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput) vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) @@ -869,7 +869,7 @@ class VLANCSVForm(forms.ModelForm): raise forms.ValidationError("Global VLAN group {} not found".format(group_name)) -class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False) diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 2587f2057..6961baf88 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -7,6 +7,7 @@ from django.db.models import Count from taggit.forms import TagField from dcim.models import Device +from extras.forms import AddRemoveTagsForm from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField from .models import Secret, SecretRole, UserKey @@ -128,7 +129,7 @@ class SecretCSVForm(forms.ModelForm): return s -class SecretBulkEditForm(BootstrapMixin, BulkEditForm): +class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput) role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False) name = forms.CharField(max_length=100, required=False) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 123b2bc24..b90934923 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -4,7 +4,7 @@ from django import forms from django.db.models import Count from taggit.forms import TagField -from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from utilities.forms import ( APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField, ) @@ -69,7 +69,7 @@ class TenantCSVForm(forms.ModelForm): } -class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput) group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 85df79f1a..1cafbfc52 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -543,6 +543,12 @@ class BulkEditView(GetReturnURLMixin, View): cfv.value = form.cleaned_data[name] cfv.save() + # Add/remove tags + if form.cleaned_data.get('add_tags', None): + obj.tags.add(*form.cleaned_data['add_tags']) + if form.cleaned_data.get('remove_tags', None): + obj.tags.remove(*form.cleaned_data['remove_tags']) + updated_count += 1 if updated_count: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index b973ed5cb..10833234b 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -10,7 +10,7 @@ from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGE from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.formfields import MACAddressFormField from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site -from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress from tenancy.forms import TenancyForm from tenancy.models import Tenant @@ -119,7 +119,7 @@ class ClusterCSVForm(forms.ModelForm): fields = Cluster.csv_headers -class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Cluster.objects.all(), widget=forms.MultipleHiddenInput) type = forms.ModelChoiceField(queryset=ClusterType.objects.all(), required=False) group = forms.ModelChoiceField(queryset=ClusterGroup.objects.all(), required=False) @@ -349,7 +349,7 @@ class VirtualMachineCSVForm(forms.ModelForm): fields = VirtualMachine.csv_headers -class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): +class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput) status = forms.ChoiceField(choices=add_blank_choice(VM_STATUS_CHOICES), required=False, initial='') cluster = forms.ModelChoiceField(queryset=Cluster.objects.all(), required=False)