From 36066068d4b96085774182036334977d37559285 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 30 Sep 2016 16:17:41 -0400 Subject: [PATCH] #527: Initial work to allow nullifying fields during bulk edit --- netbox/circuits/forms.py | 9 ++- netbox/dcim/forms.py | 63 ++++++------------- netbox/extras/forms.py | 6 +- netbox/ipam/forms.py | 40 ++++++------ netbox/project-static/js/forms.js | 5 ++ netbox/secrets/forms.py | 7 ++- .../templates/utilities/bulk_edit_form.html | 11 +++- netbox/templates/utilities/render_field.html | 25 +++++--- netbox/tenancy/forms.py | 29 ++------- netbox/utilities/forms.py | 7 +++ netbox/utilities/templatetags/form_helpers.py | 3 +- netbox/utilities/views.py | 23 +++---- 12 files changed, 106 insertions(+), 122 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f3c210dc2..b3749947b 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -3,7 +3,6 @@ from django.db.models import Count from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm -from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import ( APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea, @@ -57,6 +56,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact') comments = CommentField() + class Meta: + nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Provider @@ -178,11 +180,14 @@ class CircuitBulkEditForm(BootstrapMixin, 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) - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') comments = CommentField() + class Meta: + nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments'] + class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Circuit diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b65916267..0d5c4fbac 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -5,11 +5,11 @@ from django.db.models import Count, Q from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from ipam.models import IPAddress -from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import ( - APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, - FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, + APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField, + ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, + SlugField, ) from .models import ( @@ -42,39 +42,6 @@ def get_device_by_name_or_pk(name): return device -def bulkedit_platform_choices(): - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(p.pk, p.name) for p in Platform.objects.all()] - return choices - - -def bulkedit_rackgroup_choices(): - """ - Include an option to remove the currently assigned group from a rack. - """ - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(r.pk, r) for r in RackGroup.objects.all()] - return choices - - -def bulkedit_rackrole_choices(): - """ - Include an option to remove the currently assigned role from a rack. - """ - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(r.pk, r.name) for r in RackRole.objects.all()] - return choices - - # # Sites # @@ -114,7 +81,10 @@ class SiteImportForm(BulkImportForm, BootstrapMixin): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + + class Meta: + nullable_fields = ['tenant'] class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -234,14 +204,17 @@ class RackImportForm(BulkImportForm, BootstrapMixin): class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site') - group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group') - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') - role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role') + group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False) type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') u_height = forms.IntegerField(required=False, label='Height (U)') comments = CommentField() + class Meta: + nullable_fields = ['group', 'tenant', 'role', 'comments'] + class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Rack @@ -279,7 +252,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin): 'is_pdu', 'is_network_device', 'subdevice_role'] -class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin): +class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin): 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) @@ -583,12 +556,14 @@ class DeviceBulkEditForm(BootstrapMixin, 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') - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') - platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False, - label='Platform') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False) status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status') serial = forms.CharField(max_length=50, required=False, label='Serial Number') + class Meta: + nullable_fields = ['tenant', 'platform'] + class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 540651a01..79f980028 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -3,7 +3,7 @@ from collections import OrderedDict from django import forms from django.contrib.contenttypes.models import ContentType -from utilities.forms import LaxURLField +from utilities.forms import BulkEditForm, LaxURLField from .models import ( CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue ) @@ -49,8 +49,6 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F # Select elif cf.type == CF_TYPE_SELECT: choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] - if not cf.required: - choices = [(0, 'None')] + choices if bulk_edit or filterable_only: choices = [(None, '---------')] + choices field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required) @@ -126,7 +124,7 @@ class CustomFieldForm(forms.ModelForm): return obj -class CustomFieldBulkEditForm(forms.Form): +class CustomFieldBulkEditForm(BulkEditForm): custom_fields = [] def __init__(self, model, *args, **kwargs): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 6382895f8..6586c3435 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -3,7 +3,6 @@ from django.db.models import Count from dcim.models import Site, Device, Interface from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm -from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import ( APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, @@ -23,18 +22,6 @@ IP_FAMILY_CHOICES = [ ] -def bulkedit_vrf_choices(): - """ - Include an option to assign the object to the global table. - """ - choices = [ - (None, '---------'), - (0, 'Global'), - ] - choices += [(v.pk, v.name) for v in VRF.objects.all()] - return choices - - # # VRFs # @@ -67,9 +54,12 @@ class VRFImportForm(BulkImportForm, BootstrapMixin): class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) + class Meta: + nullable_fields = ['tenant', 'description'] + class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VRF @@ -124,6 +114,9 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): date_added = forms.DateField(required=False) description = forms.CharField(max_length=100, required=False) + class Meta: + nullable_fields = ['date_added', 'description'] + class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Aggregate @@ -253,12 +246,15 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin): class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) - vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF') - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) + class Meta: + nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description'] + def prefix_status_choices(): status_counts = {} @@ -407,10 +403,13 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin): class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput) - vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF') - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) + class Meta: + nullable_fields = ['vrf', 'tenant', 'description'] + class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): model = IPAddress @@ -509,11 +508,14 @@ class VLANBulkEditForm(BootstrapMixin, 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) - tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) + class Meta: + nullable_fields = ['group', 'tenant', 'role', 'description'] + def vlan_status_choices(): status_counts = {} diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index d437c3e4a..944b01868 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -37,6 +37,11 @@ $(document).ready(function() { }) } + // Bulk edit nullification + $('input:checkbox[name=_nullify]').click(function (event) { + $('#id_' + this.value).toggle('disabled'); + }); + // API select widget $('select[filter-for]').change(function () { diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index cd2a77ae3..a163640b8 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -5,7 +5,7 @@ from django import forms from django.db.models import Count from dcim.models import Device -from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField +from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField from .models import Secret, SecretRole, UserKey @@ -89,11 +89,14 @@ class SecretImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'})) -class SecretBulkEditForm(forms.Form, BootstrapMixin): +class SecretBulkEditForm(BulkEditForm, BootstrapMixin): 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) + class Meta: + nullable_fields = ['name'] + class SecretFilterForm(forms.Form, BootstrapMixin): role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug') diff --git a/netbox/templates/utilities/bulk_edit_form.html b/netbox/templates/utilities/bulk_edit_form.html index 98c07519f..24a1b1077 100644 --- a/netbox/templates/utilities/bulk_edit_form.html +++ b/netbox/templates/utilities/bulk_edit_form.html @@ -8,6 +8,9 @@ {% if request.POST.redirect_url %} {% endif %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %}
@@ -29,7 +32,13 @@
{% block form_title %}Attributes{% endblock %}
- {% render_form form %} + {% for field in form.visible_fields %} + {% if field.name in form.nullable_fields %} + {% render_field field bulk_nullable=True %} + {% else %} + {% render_field field %} + {% endif %} + {% endfor %}
diff --git a/netbox/templates/utilities/render_field.html b/netbox/templates/utilities/render_field.html index b53c5ab3e..09a91f752 100644 --- a/netbox/templates/utilities/render_field.html +++ b/netbox/templates/utilities/render_field.html @@ -5,26 +5,26 @@
{% if field.help_text %} {{ field.help_text|safe }} {% endif %}
-
- {% elif field|widget_type == 'radioselect' %} -
-
-
+ {% endif %}
{% elif field|widget_type == 'textarea' %}
{{ field }} + {% if bulk_nullable %} + + {% endif %} {% if field.help_text %} {{ field.help_text|safe }} {% endif %} @@ -40,6 +40,11 @@
{{ field }} + {% if bulk_nullable %} + + {% endif %} {% if field.help_text %} {{ field.help_text|safe }} {% endif %} diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index b49e972a7..0fe6d933f 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -7,30 +7,6 @@ from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDat from .models import Tenant, TenantGroup -def bulkedit_tenantgroup_choices(): - """ - Include an option to remove the currently assigned TenantGroup from a Tenant. - """ - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(g.pk, g.name) for g in TenantGroup.objects.all()] - return choices - - -def bulkedit_tenant_choices(): - """ - Include an option to remove the currently assigned Tenant from an object. - """ - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(t.pk, t.name) for t in Tenant.objects.all()] - return choices - - # # Tenant groups # @@ -71,7 +47,10 @@ class TenantImportForm(BulkImportForm, BootstrapMixin): class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput) - group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group') + group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False) + + class Meta: + nullable_fields = ['group'] class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 1574f4aff..e27b59e65 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -294,6 +294,13 @@ class ConfirmationForm(forms.Form, BootstrapMixin): confirm = forms.BooleanField(required=True) +class BulkEditForm(forms.Form): + + def __init__(self, *args, **kwargs): + super(BulkEditForm, self).__init__(*args, **kwargs) + self.nullable_fields = getattr(self.Meta, 'nullable_fields') + + class BulkImportForm(forms.Form): def clean(self): diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 3d7540cc7..572be15fe 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -5,12 +5,13 @@ register = template.Library() @register.inclusion_tag('utilities/render_field.html') -def render_field(field): +def render_field(field, bulk_nullable=False): """ Render a single form field from template """ return { 'field': field, + 'bulk_nullable': bulk_nullable, } diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 5e8b01d6f..d0ac4e26a 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -310,8 +310,15 @@ class BulkEditView(View): custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk'] - # Update objects - updated_count = self.update_objects(pk_list, form, standard_fields) + # Update standard fields. If a field is listed in _nullify, delete its value. + nullified_fields = request.POST.getlist('_nullify') + fields_to_update = {} + for field in standard_fields: + if field in form.nullable_fields and field in nullified_fields: + fields_to_update[field] = '' + elif form.cleaned_data[field]: + fields_to_update[field] = form.cleaned_data[field] + updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update) # Update custom fields for objects if custom_fields: @@ -342,18 +349,6 @@ class BulkEditView(View): 'cancel_url': redirect_url, }) - def update_objects(self, pk_list, form, fields): - fields_to_update = {} - - for name in fields: - # Check for zero value (bulk editing) - if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0: - fields_to_update[name] = None - elif form.cleaned_data[name]: - fields_to_update[name] = form.cleaned_data[name] - - return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update) - def update_custom_fields(self, pk_list, form, fields): obj_type = ContentType.objects.get_for_model(self.cls) objs_updated = False